1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-26 16:09:27 -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:
Bartek Iwańczuk 2020-12-10 14:45:41 +01:00 committed by GitHub
parent b7faa27704
commit f91fa16661
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 258 additions and 326 deletions

View file

@ -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,
};

View file

@ -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;
}

View file

@ -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
View 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);

View file

@ -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();
}
}