mirror of
https://github.com/denoland/deno.git
synced 2024-12-27 09:39:08 -05:00
refactor(core): stack trace mapping (#8660)
This commit adds "Deno.core.createPrepareStackTrace". This function was moved from "cli/rt/40_error_stack.js" to unify handling of stack frames in core (before this PR there was implicit dependency on logic in "core/error.rs::JsError"). Unfortunately formatting logic must still be duplicated in "cli/error.js::PrettyJsError" to provide coloring, but currently there's no solution to this problem. "createPrepareStackTrace" can accept a single argument; a function that takes a location and provides source mapped location back.
This commit is contained in:
parent
b7faa27704
commit
f91fa16661
5 changed files with 258 additions and 326 deletions
|
@ -1,11 +1,7 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
((window) => {
|
||||
// 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
|
||||
const core = window.Deno.core;
|
||||
const assert = window.__bootstrap.util.assert;
|
||||
const internals = window.__bootstrap.internals;
|
||||
|
||||
function opFormatDiagnostics(diagnostics) {
|
||||
return core.jsonOpSync("op_format_diagnostic", diagnostics);
|
||||
|
@ -20,229 +16,7 @@
|
|||
};
|
||||
}
|
||||
|
||||
function patchCallSite(callSite, location) {
|
||||
return {
|
||||
getThis() {
|
||||
return callSite.getThis();
|
||||
},
|
||||
getTypeName() {
|
||||
return callSite.getTypeName();
|
||||
},
|
||||
getFunction() {
|
||||
return callSite.getFunction();
|
||||
},
|
||||
getFunctionName() {
|
||||
return callSite.getFunctionName();
|
||||
},
|
||||
getMethodName() {
|
||||
return callSite.getMethodName();
|
||||
},
|
||||
getFileName() {
|
||||
return location.fileName;
|
||||
},
|
||||
getLineNumber() {
|
||||
return location.lineNumber;
|
||||
},
|
||||
getColumnNumber() {
|
||||
return location.columnNumber;
|
||||
},
|
||||
getEvalOrigin() {
|
||||
return callSite.getEvalOrigin();
|
||||
},
|
||||
isToplevel() {
|
||||
return callSite.isToplevel();
|
||||
},
|
||||
isEval() {
|
||||
return callSite.isEval();
|
||||
},
|
||||
isNative() {
|
||||
return callSite.isNative();
|
||||
},
|
||||
isConstructor() {
|
||||
return callSite.isConstructor();
|
||||
},
|
||||
isAsync() {
|
||||
return callSite.isAsync();
|
||||
},
|
||||
isPromiseAll() {
|
||||
return callSite.isPromiseAll();
|
||||
},
|
||||
getPromiseIndex() {
|
||||
return callSite.getPromiseIndex();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Keep in sync with `cli/fmt_errors.rs`.
|
||||
function formatLocation(callSite) {
|
||||
if (callSite.isNative()) {
|
||||
return "native";
|
||||
}
|
||||
|
||||
let result = "";
|
||||
|
||||
const fileName = callSite.getFileName();
|
||||
|
||||
if (fileName) {
|
||||
result += fileName;
|
||||
} else {
|
||||
if (callSite.isEval()) {
|
||||
const evalOrigin = callSite.getEvalOrigin();
|
||||
assert(evalOrigin != null);
|
||||
result += `${evalOrigin}, `;
|
||||
}
|
||||
result += "<anonymous>";
|
||||
}
|
||||
|
||||
const lineNumber = callSite.getLineNumber();
|
||||
if (lineNumber != null) {
|
||||
result += `:${lineNumber}`;
|
||||
|
||||
const columnNumber = callSite.getColumnNumber();
|
||||
if (columnNumber != null) {
|
||||
result += `:${columnNumber}`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep in sync with `cli/fmt_errors.rs`.
|
||||
function formatCallSite(callSite) {
|
||||
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) {
|
||||
const typeName = callSite.getTypeName();
|
||||
const methodName = callSite.getMethodName();
|
||||
|
||||
if (functionName) {
|
||||
if (typeName) {
|
||||
if (!functionName.startsWith(typeName)) {
|
||||
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>";
|
||||
}
|
||||
}
|
||||
} else if (isConstructor) {
|
||||
result += "new ";
|
||||
if (functionName) {
|
||||
result += functionName;
|
||||
} else {
|
||||
result += "<anonymous>";
|
||||
}
|
||||
} else if (functionName) {
|
||||
result += functionName;
|
||||
} else {
|
||||
result += formatLocation(callSite);
|
||||
return result;
|
||||
}
|
||||
|
||||
result += ` (${formatLocation(callSite)})`;
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateCallSite(callSite) {
|
||||
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,
|
||||
callSites,
|
||||
) {
|
||||
const mappedCallSites = callSites.map(
|
||||
(callSite) => {
|
||||
const fileName = callSite.getFileName();
|
||||
const lineNumber = callSite.getLineNumber();
|
||||
const columnNumber = callSite.getColumnNumber();
|
||||
if (fileName && lineNumber != null && columnNumber != null) {
|
||||
return patchCallSite(
|
||||
callSite,
|
||||
opApplySourceMap({
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return callSite;
|
||||
},
|
||||
);
|
||||
Object.defineProperties(error, {
|
||||
__callSiteEvals: { value: [], configurable: true },
|
||||
});
|
||||
const formattedCallSites = [];
|
||||
for (const callSite of mappedCallSites) {
|
||||
error.__callSiteEvals.push(Object.freeze(evaluateCallSite(callSite)));
|
||||
formattedCallSites.push(formatCallSite(callSite));
|
||||
}
|
||||
Object.freeze(error.__callSiteEvals);
|
||||
const message = error.message !== undefined ? error.message : "";
|
||||
const name = error.name !== undefined ? error.name : "Error";
|
||||
let messageLine;
|
||||
if (name != "" && message != "") {
|
||||
messageLine = `${name}: ${message}`;
|
||||
} else if ((name || message) != "") {
|
||||
messageLine = name || message;
|
||||
} else {
|
||||
messageLine = "";
|
||||
}
|
||||
return messageLine +
|
||||
formattedCallSites.map((s) => `\n at ${s}`).join("");
|
||||
}
|
||||
|
||||
function setPrepareStackTrace(ErrorConstructor) {
|
||||
ErrorConstructor.prepareStackTrace = prepareStackTrace;
|
||||
}
|
||||
|
||||
internals.exposeForTest("setPrepareStackTrace", setPrepareStackTrace);
|
||||
|
||||
window.__bootstrap.errorStack = {
|
||||
setPrepareStackTrace,
|
||||
opApplySourceMap,
|
||||
opFormatDiagnostics,
|
||||
};
|
||||
|
|
|
@ -164,9 +164,16 @@ delete Object.prototype.__proto__;
|
|||
// TODO(bartlomieju): a very crude way to disable
|
||||
// source mapping of errors. This condition is true
|
||||
// only for compiled standalone binaries.
|
||||
let prepareStackTrace;
|
||||
if (s.applySourceMaps) {
|
||||
errorStack.setPrepareStackTrace(Error);
|
||||
prepareStackTrace = core.createPrepareStackTrace(
|
||||
errorStack.opApplySourceMap,
|
||||
);
|
||||
} else {
|
||||
prepareStackTrace = core.createPrepareStackTrace();
|
||||
}
|
||||
Error.prepareStackTrace = prepareStackTrace;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,87 +1,6 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
import { assert, assertEquals, assertMatch, unitTest } from "./test_util.ts";
|
||||
|
||||
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
|
||||
const { setPrepareStackTrace } = Deno[Deno.internal];
|
||||
|
||||
interface CallSite {
|
||||
getThis(): unknown;
|
||||
getTypeName(): string | null;
|
||||
// deno-lint-ignore ban-types
|
||||
getFunction(): Function | null;
|
||||
getFunctionName(): string | null;
|
||||
getMethodName(): string | null;
|
||||
getFileName(): string | null;
|
||||
getLineNumber(): number | null;
|
||||
getColumnNumber(): number | null;
|
||||
getEvalOrigin(): string | null;
|
||||
isToplevel(): boolean | null;
|
||||
isEval(): boolean;
|
||||
isNative(): boolean;
|
||||
isConstructor(): boolean;
|
||||
isAsync(): boolean;
|
||||
isPromiseAll(): boolean;
|
||||
getPromiseIndex(): number | null;
|
||||
}
|
||||
|
||||
function getMockCallSite(
|
||||
fileName: string,
|
||||
lineNumber: number | null,
|
||||
columnNumber: number | null,
|
||||
): CallSite {
|
||||
return {
|
||||
getThis(): unknown {
|
||||
return undefined;
|
||||
},
|
||||
getTypeName(): string {
|
||||
return "";
|
||||
},
|
||||
// deno-lint-ignore ban-types
|
||||
getFunction(): Function {
|
||||
return (): void => {};
|
||||
},
|
||||
getFunctionName(): string {
|
||||
return "";
|
||||
},
|
||||
getMethodName(): string {
|
||||
return "";
|
||||
},
|
||||
getFileName(): string {
|
||||
return fileName;
|
||||
},
|
||||
getLineNumber(): number | null {
|
||||
return lineNumber;
|
||||
},
|
||||
getColumnNumber(): number | null {
|
||||
return columnNumber;
|
||||
},
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
unitTest(function errorStackMessageLine(): void {
|
||||
const e1 = new Error();
|
||||
e1.name = "Foo";
|
||||
|
@ -122,24 +41,6 @@ unitTest(function errorStackMessageLine(): void {
|
|||
assertMatch(e6.stack!, /^null: null\n/);
|
||||
});
|
||||
|
||||
// FIXME(bartlomieju): no longer works after migrating
|
||||
// to JavaScript runtime code
|
||||
unitTest({ ignore: true }, function prepareStackTrace(): void {
|
||||
// deno-lint-ignore 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("CLI_SNAPSHOT.js", 23, 0),
|
||||
]);
|
||||
assert(result.startsWith("Error: foo\n"));
|
||||
assert(result.includes(".ts:"), "should remap to something in 'js/'");
|
||||
});
|
||||
|
||||
unitTest(function captureStackTrace(): void {
|
||||
function foo(): void {
|
||||
const error = new Error();
|
||||
|
|
247
core/error.js
Normal file
247
core/error.js
Normal file
|
@ -0,0 +1,247 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
((window) => {
|
||||
// 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
|
||||
function patchCallSite(callSite, location) {
|
||||
return {
|
||||
getThis() {
|
||||
return callSite.getThis();
|
||||
},
|
||||
getTypeName() {
|
||||
return callSite.getTypeName();
|
||||
},
|
||||
getFunction() {
|
||||
return callSite.getFunction();
|
||||
},
|
||||
getFunctionName() {
|
||||
return callSite.getFunctionName();
|
||||
},
|
||||
getMethodName() {
|
||||
return callSite.getMethodName();
|
||||
},
|
||||
getFileName() {
|
||||
return location.fileName;
|
||||
},
|
||||
getLineNumber() {
|
||||
return location.lineNumber;
|
||||
},
|
||||
getColumnNumber() {
|
||||
return location.columnNumber;
|
||||
},
|
||||
getEvalOrigin() {
|
||||
return callSite.getEvalOrigin();
|
||||
},
|
||||
isToplevel() {
|
||||
return callSite.isToplevel();
|
||||
},
|
||||
isEval() {
|
||||
return callSite.isEval();
|
||||
},
|
||||
isNative() {
|
||||
return callSite.isNative();
|
||||
},
|
||||
isConstructor() {
|
||||
return callSite.isConstructor();
|
||||
},
|
||||
isAsync() {
|
||||
return callSite.isAsync();
|
||||
},
|
||||
isPromiseAll() {
|
||||
return callSite.isPromiseAll();
|
||||
},
|
||||
getPromiseIndex() {
|
||||
return callSite.getPromiseIndex();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Keep in sync with `cli/fmt_errors.rs`.
|
||||
function formatLocation(callSite) {
|
||||
if (callSite.isNative()) {
|
||||
return "native";
|
||||
}
|
||||
|
||||
let result = "";
|
||||
|
||||
const fileName = callSite.getFileName();
|
||||
|
||||
if (fileName) {
|
||||
result += fileName;
|
||||
} else {
|
||||
if (callSite.isEval()) {
|
||||
const evalOrigin = callSite.getEvalOrigin();
|
||||
if (evalOrigin == null) {
|
||||
throw new Error("assert evalOrigin");
|
||||
}
|
||||
result += `${evalOrigin}, `;
|
||||
}
|
||||
result += "<anonymous>";
|
||||
}
|
||||
|
||||
const lineNumber = callSite.getLineNumber();
|
||||
if (lineNumber != null) {
|
||||
result += `:${lineNumber}`;
|
||||
|
||||
const columnNumber = callSite.getColumnNumber();
|
||||
if (columnNumber != null) {
|
||||
result += `:${columnNumber}`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep in sync with `cli/fmt_errors.rs`.
|
||||
function formatCallSite(callSite) {
|
||||
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) {
|
||||
const typeName = callSite.getTypeName();
|
||||
const methodName = callSite.getMethodName();
|
||||
|
||||
if (functionName) {
|
||||
if (typeName) {
|
||||
if (!functionName.startsWith(typeName)) {
|
||||
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>";
|
||||
}
|
||||
}
|
||||
} else if (isConstructor) {
|
||||
result += "new ";
|
||||
if (functionName) {
|
||||
result += functionName;
|
||||
} else {
|
||||
result += "<anonymous>";
|
||||
}
|
||||
} else if (functionName) {
|
||||
result += functionName;
|
||||
} else {
|
||||
result += formatLocation(callSite);
|
||||
return result;
|
||||
}
|
||||
|
||||
result += ` (${formatLocation(callSite)})`;
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluateCallSite(callSite) {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that can be used as `Error.prepareStackTrace`.
|
||||
*
|
||||
* This function accepts an optional argument, a function that performs
|
||||
* source mapping. It is not required to pass this argument, but
|
||||
* in such case only JavaScript sources will have proper position in
|
||||
* stack frames.
|
||||
* @param {(
|
||||
* fileName: string,
|
||||
* lineNumber: number,
|
||||
* columnNumber: number
|
||||
* ) => {
|
||||
* fileName: string,
|
||||
* lineNumber: number,
|
||||
* columnNumber: number
|
||||
* }} sourceMappingFn
|
||||
*/
|
||||
function createPrepareStackTrace(sourceMappingFn) {
|
||||
return function prepareStackTrace(
|
||||
error,
|
||||
callSites,
|
||||
) {
|
||||
const mappedCallSites = callSites.map(
|
||||
(callSite) => {
|
||||
const fileName = callSite.getFileName();
|
||||
const lineNumber = callSite.getLineNumber();
|
||||
const columnNumber = callSite.getColumnNumber();
|
||||
if (
|
||||
sourceMappingFn && fileName && lineNumber != null &&
|
||||
columnNumber != null
|
||||
) {
|
||||
return patchCallSite(
|
||||
callSite,
|
||||
sourceMappingFn({
|
||||
fileName,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return callSite;
|
||||
},
|
||||
);
|
||||
Object.defineProperties(error, {
|
||||
__callSiteEvals: { value: [], configurable: true },
|
||||
});
|
||||
const formattedCallSites = [];
|
||||
for (const callSite of mappedCallSites) {
|
||||
error.__callSiteEvals.push(evaluateCallSite(callSite));
|
||||
formattedCallSites.push(formatCallSite(callSite));
|
||||
}
|
||||
const message = error.message !== undefined ? error.message : "";
|
||||
const name = error.name !== undefined ? error.name : "Error";
|
||||
let messageLine;
|
||||
if (name != "" && message != "") {
|
||||
messageLine = `${name}: ${message}`;
|
||||
} else if ((name || message) != "") {
|
||||
messageLine = name || message;
|
||||
} else {
|
||||
messageLine = "";
|
||||
}
|
||||
return messageLine +
|
||||
formattedCallSites.map((s) => `\n at ${s}`).join("");
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(window.Deno.core, {
|
||||
createPrepareStackTrace,
|
||||
});
|
||||
})(this);
|
|
@ -329,6 +329,9 @@ impl JsRuntime {
|
|||
self
|
||||
.execute("deno:core/core.js", include_str!("core.js"))
|
||||
.unwrap();
|
||||
self
|
||||
.execute("deno:core/error.js", include_str!("error.js"))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue