mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Capture stack traces with Error.captureStackTrace as Node does
This commit is contained in:
@@ -7586,4 +7586,5 @@ declare module "node:stream/web" {
|
||||
}
|
||||
interface ErrorConstructor {
|
||||
stackTraceLimit: number;
|
||||
captureStackTrace(targetObject: object, constructorOpt?: Function): void;
|
||||
}
|
||||
|
||||
@@ -677,6 +677,32 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
|
||||
export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "error.captureStackTrace") {
|
||||
// The second argument names the function constructorOpt skips through;
|
||||
// its frame text is the constant its own frame pushes (emit-function.ts),
|
||||
// or null when it has no frame, which leaves no frames.
|
||||
const target = host.emitExpr(e.args[0]!);
|
||||
const named = e.args[1]!;
|
||||
const skip = e.args[2]!.kind === "boolLit" && e.args[2]!.value;
|
||||
let through = "null";
|
||||
if (named.kind === "strLit" && named.value !== "") {
|
||||
const fn = host.mod.functions.find((f) => f.name === named.value);
|
||||
if (
|
||||
fn !== undefined &&
|
||||
host.stackTraces &&
|
||||
!host.wasi &&
|
||||
fn.sourceName !== undefined &&
|
||||
!host.unframedFunctions.has(fn.name)
|
||||
)
|
||||
through = host.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
|
||||
}
|
||||
host.declare(`declare void @scr_error_capture_stack_trace(ptr, ptr, i1)`);
|
||||
B.line(
|
||||
`call void @scr_error_capture_stack_trace(ptr ${target.name}, ptr ${through}, i1 ${skip ? "true" : "false"})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "error.argTypeThrow") {
|
||||
// Always throws with the runtime-rendered Received tail (the
|
||||
// error.nodeThrow dummy pattern). Borrows all three.
|
||||
|
||||
@@ -1506,7 +1506,6 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.objectPrototype": "scr_dyn_object_prototype",
|
||||
"dyn.errorPrototype": "scr_dyn_error_prototype",
|
||||
"dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype",
|
||||
"dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace",
|
||||
"dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function",
|
||||
"dyn.functionApply": "scr_dyn_function_apply",
|
||||
"dyn.builtinMethod": "scr_dyn_builtin_method",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { dynUndefinedExpr } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
VOID,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
|
||||
@@ -324,60 +324,6 @@ export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
: error;
|
||||
}
|
||||
|
||||
/** `Error.captureStackTrace(target[, constructorOpt])` (also called as
|
||||
* `Error.captureStackTrace?.(...)`, which always calls it). On a
|
||||
* checked-dynamic target (`this` in a JavaScript constructor function) the
|
||||
* runtime defines target's `stack`. An instance of a class that extends
|
||||
* Error derives its stack from its name and message when the stack is
|
||||
* read, so the call only evaluates its arguments. Both arguments are
|
||||
* evaluated; the second only selects frames in Node. Null for other
|
||||
* targets and callees. */
|
||||
export function lowerErrorCaptureStackTraceCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.questionDotToken) return null;
|
||||
if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null;
|
||||
const args = call.arguments.nodes;
|
||||
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
|
||||
const probed = tryLowerExpression(lowerer, args[0]!);
|
||||
const loc = locOf(call);
|
||||
if (
|
||||
probed?.type.kind === "object" &&
|
||||
(probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error"))
|
||||
) {
|
||||
const stmts: IrStmt[] = [];
|
||||
for (const arg of args) {
|
||||
const value = lowerer.lowerExpr(arg);
|
||||
if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc });
|
||||
}
|
||||
const result = dynUndefinedExpr(loc);
|
||||
return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
|
||||
}
|
||||
if (probed?.type.kind !== "dyn") return null;
|
||||
const target = lowerer.lowerExpr(args[0]!);
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.errorCaptureStackTrace",
|
||||
args: [target],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
if (args.length === 1) return result;
|
||||
const local = lowerer.declareHiddenLocal("%captureTarget", DYN);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: local.id, init: target, loc },
|
||||
{ kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc },
|
||||
],
|
||||
result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Error.isError(value)`. A catch binding tests its payload against the
|
||||
* Error class, an `unknown` value tests the checked-dynamic tree's error
|
||||
* encoding, and a union tests its runtime tag against the arms whose class
|
||||
@@ -470,3 +416,120 @@ export function lowerErrorIsErrorCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The class whose constructor body `node` is directly in (not inside a
|
||||
* nested function): while it runs, `new` of that class is on the stack. */
|
||||
function enclosingConstructorClass(node: ts.Node): ts.ClassLikeDeclaration | undefined {
|
||||
for (let n = node.parent; n !== undefined; n = n.parent) {
|
||||
if (ts.isConstructorDeclaration(n))
|
||||
return ts.isClassDeclaration(n.parent) || ts.isClassExpression(n.parent)
|
||||
? n.parent
|
||||
: undefined;
|
||||
if (ts.isFunctionLike(n) && !ts.isArrowFunction(n)) return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** `Error.captureStackTrace(target, constructorOpt?)`, also through `?.`:
|
||||
* target.stack becomes the target's heading and the current stack frames,
|
||||
* as V8 does it (scr_error_capture_stack_trace). The target is an error, a
|
||||
* checked-dynamic object, or a primitive (a TypeError at run time).
|
||||
* constructorOpt leaves out the frames up to and including its topmost call:
|
||||
* a function declaration skips through its own frame; `this.constructor`,
|
||||
* `new.target` or the class's own name inside a class constructor leave the
|
||||
* frames from the code that ran `new`, which are the current frames
|
||||
* (constructors have no frames of their own). Other targets are refused,
|
||||
* and so are other constructorOpt values in TypeScript; JavaScript keeps all
|
||||
* frames for them. Null when the callee is not the
|
||||
* standard library's Error.captureStackTrace. */
|
||||
export function lowerErrorCaptureStackTraceCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null;
|
||||
const args = call.arguments.nodes;
|
||||
const loc = locOf(call);
|
||||
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
"Error.captureStackTrace arguments",
|
||||
call,
|
||||
"pass a target and an optional constructor",
|
||||
);
|
||||
}
|
||||
const target = lowerer.lowerExpr(args[0]!);
|
||||
// Primitives (and unions of them) reach the runtime, which throws V8's
|
||||
// TypeError for them; objects without a checked-dynamic view are refused,
|
||||
// since a boxed copy would not show the stack.
|
||||
const primitive =
|
||||
["f64", "string", "bool", "nullT", "undefinedT", "bigint"].includes(target.type.kind) ||
|
||||
(target.type.kind === "union" &&
|
||||
lowerer.dynConvertible(target.type) &&
|
||||
(lowerer.unions.get(target.type.unionId)?.arms ?? []).every((arm) =>
|
||||
["f64", "string", "bool", "nullT", "undefinedT", "bigint"].includes(arm.kind),
|
||||
));
|
||||
if (target.type.kind !== "dyn" && !primitive && !isErrorClassType(lowerer, target.type)) {
|
||||
lowerer.noLowering(
|
||||
`Error.captureStackTrace on '${lowerer.fmt(target.type)}' values`,
|
||||
args[0]!,
|
||||
"pass an Error instance, or an untyped object in JavaScript",
|
||||
);
|
||||
}
|
||||
let through = "";
|
||||
let skip = false;
|
||||
const ctor = args[1];
|
||||
if (ctor !== undefined) {
|
||||
let node: ts.Expression = ctor;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
const symbol = ts.isIdentifier(node) ? lowerer.checker.getSymbolAtLocation(node) : undefined;
|
||||
const resolved =
|
||||
symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(symbol)
|
||||
: symbol;
|
||||
const cls = enclosingConstructorClass(call);
|
||||
const constructing =
|
||||
(ts.isPropertyAccessExpression(node) &&
|
||||
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
node.name.text === "constructor") ||
|
||||
(ts.isMetaProperty(node) && node.name.text === "target") ||
|
||||
(resolved !== undefined &&
|
||||
cls?.name !== undefined &&
|
||||
lowerer.checker.getSymbolAtLocation(cls.name) === resolved);
|
||||
const sig = resolved !== undefined ? lowerer.fnSigsBySymbol.get(resolved) : undefined;
|
||||
if (constructing && cls !== undefined) {
|
||||
skip = false;
|
||||
} else if (sig !== undefined) {
|
||||
lowerer.noteEdge(sig.name);
|
||||
through = sig.name;
|
||||
skip = true;
|
||||
} else if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
// JavaScript (undici's `captureStackTrace(err, module.exports.fetch)`)
|
||||
// keeps every frame: the same frames V8 keeps, and the ones above them.
|
||||
skip = false;
|
||||
} else {
|
||||
lowerer.noLowering(
|
||||
"Error.captureStackTrace with this constructor argument",
|
||||
ctor,
|
||||
"pass a function declaration, or this.constructor or the class inside its constructor",
|
||||
);
|
||||
}
|
||||
}
|
||||
const capture: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "error.captureStackTrace",
|
||||
args: [
|
||||
lowerer.coerceToExpected(target, DYN),
|
||||
{ kind: "strLit", value: through, type: STRING, loc },
|
||||
{ kind: "boolLit", value: skip, type: BOOL, loc },
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: capture, loc }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -418,6 +418,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const timeoutOptional = lowerer.lowerTimeoutMethodCall(expr, expr.expression);
|
||||
if (timeoutOptional) return timeoutOptional;
|
||||
}
|
||||
// `Error.captureStackTrace?.(this, this.constructor)`: Node always has it.
|
||||
if (
|
||||
ts.isPropertyAccessExpression(expr.expression) &&
|
||||
(expr.questionDotToken || expr.expression.questionDotToken)
|
||||
) {
|
||||
const captured = lowerErrorCaptureStackTraceCall(lowerer, expr, expr.expression);
|
||||
if (captured) return captured;
|
||||
}
|
||||
// `req.session?.m(...)` remains absent on compatibility requests. Live
|
||||
// `req.stream` calls now pass through the normal optional-chain and h2
|
||||
// stream lowering using lowerServerProperty's checked getter.
|
||||
|
||||
@@ -443,6 +443,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"js.toNumberCoerce",
|
||||
// Error messages use the same coercion protocol before installing cause.
|
||||
"error.newOptions",
|
||||
"error.captureStackTrace",
|
||||
"error.ctorOptions",
|
||||
"error.newAggregate",
|
||||
"error.newSuppressed",
|
||||
@@ -610,9 +611,6 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.objCreateWithProperties",
|
||||
"dyn.getPrototype",
|
||||
"dyn.setPrototype",
|
||||
// Error.captureStackTrace: Node's TypeError for a primitive target, and
|
||||
// the error string conversion of name and message
|
||||
"dyn.errorCaptureStackTrace",
|
||||
// a proxy in the prototype chain can throw
|
||||
"dyn.builtinInstanceOf",
|
||||
"dyn.functionInstanceOf",
|
||||
|
||||
@@ -3369,6 +3369,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* Options retain cause presence; constructor calls borrow their args.
|
||||
* Message coercion may throw before the cause is installed. */
|
||||
"error.newOptions": { argTypes: [DYN, DYN], result: VOID },
|
||||
"error.captureStackTrace": { argTypes: [DYN, STRING, BOOL], result: VOID },
|
||||
"error.ctorOptions": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
/** Borrowed Error receiver. cause returns an owned dyn value (undefined
|
||||
* when absent); hasCause distinguishes absence from present undefined. */
|
||||
@@ -3682,7 +3683,6 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dyn.objectPrototype": { argTypes: [], result: DYN },
|
||||
"dyn.errorPrototype": { argTypes: [], result: DYN },
|
||||
"dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL },
|
||||
"dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN },
|
||||
"dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN },
|
||||
"dyn.functionApply": { argTypes: [], result: DYN },
|
||||
"dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN },
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6873-error-capture-stack-trace.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6874-error-capture-stack-trace-objects.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/error-capture-stack-trace-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1092,7 +1092,6 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt
|
||||
ScrDyn *scr_dyn_object_prototype(void);
|
||||
ScrDyn *scr_dyn_error_prototype(void);
|
||||
bool scr_dyn_inherits_error_prototype(const ScrDyn *value);
|
||||
ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target);
|
||||
ScrDyn *scr_dyn_error_capture_stack_trace_function(void);
|
||||
ScrDyn *scr_dyn_function_apply(void);
|
||||
ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list);
|
||||
@@ -1195,6 +1194,7 @@ ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */
|
||||
/* The cache's runtime-internal access pair (scr_json.c owns the storage;
|
||||
* the gated extraction reads/writes through these). */
|
||||
ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */
|
||||
void scr_error_capture_stack_trace(ScrDyn *target, const char *through, bool skip);
|
||||
void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */
|
||||
/* Sets an own property on an error: its checked-dynamic node, which casts
|
||||
* and util.inspect read. Moves value. */
|
||||
|
||||
@@ -196,19 +196,21 @@ void scr_stack_limit_set(double limit) {
|
||||
scr_stack_limit = limit;
|
||||
}
|
||||
|
||||
ScrStr *scr_stack_capture(void) {
|
||||
if (!SCR_EXC_CUR()->stack) return NULL;
|
||||
/* The frames from `first` down, at most Error.stackTraceLimit of them, one
|
||||
* "\n at ..." line each; NULL when there are none. */
|
||||
static ScrStr *scr_stack_capture_from(ScrStackFrame *first) {
|
||||
if (!first) return NULL;
|
||||
const double limit = floor(scr_stack_limit);
|
||||
if (!(limit > 0)) return NULL;
|
||||
size_t length = 0;
|
||||
size_t count = 0;
|
||||
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame && (double)count++ < limit; frame = frame->previous)
|
||||
for (ScrStackFrame *frame = first; frame && (double)count++ < limit; frame = frame->previous)
|
||||
length += 1 + strlen(frame->text);
|
||||
char *text = malloc(length);
|
||||
if (!text) scr_trap("scriptc: out of memory\n");
|
||||
size_t offset = 0;
|
||||
count = 0;
|
||||
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame && (double)count++ < limit; frame = frame->previous) {
|
||||
for (ScrStackFrame *frame = first; frame && (double)count++ < limit; frame = frame->previous) {
|
||||
text[offset++] = '\n';
|
||||
size_t size = strlen(frame->text);
|
||||
memcpy(text + offset, frame->text, size);
|
||||
@@ -219,6 +221,20 @@ ScrStr *scr_stack_capture(void) {
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrStr *scr_stack_capture(void) {
|
||||
return scr_stack_capture_from(SCR_EXC_CUR()->stack);
|
||||
}
|
||||
|
||||
/* The frames below the topmost frame whose text is `through` (a function's
|
||||
* " at name (file)" line), as Error.captureStackTrace's constructorOpt
|
||||
* leaves them; NULL when no such frame is on the stack. */
|
||||
ScrStr *scr_stack_capture_after(const char *through) {
|
||||
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame; frame = frame->previous)
|
||||
if (frame->text == through || strcmp(frame->text, through) == 0)
|
||||
return scr_stack_capture_from(frame->previous);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#define scr_exc_kind (SCR_EXC_CUR()->kind)
|
||||
#define scr_exc_f64 (SCR_EXC_CUR()->f64)
|
||||
#define scr_exc_bool (SCR_EXC_CUR()->b)
|
||||
|
||||
@@ -68,6 +68,7 @@ typedef struct ScrStackFrame {
|
||||
void scr_stack_enter(ScrStackFrame *frame, const char *text);
|
||||
void scr_stack_leave(ScrStackFrame *frame);
|
||||
ScrStr *scr_stack_capture(void); /* +1, NULL when no source frames exist */
|
||||
ScrStr *scr_stack_capture_after(const char *through); /* +1, NULL when none remain */
|
||||
double scr_stack_limit_get(void);
|
||||
void scr_stack_limit_set(double limit);
|
||||
|
||||
|
||||
+106
-44
@@ -1950,52 +1950,13 @@ ScrDyn *scr_dyn_error_prototype(void) {
|
||||
return scr_dyn_retain(scr_builtin_error_prototype);
|
||||
}
|
||||
|
||||
/* Error.captureStackTrace(target): defines a non-enumerable own `stack`
|
||||
* on target. Node's value is the Error.prototype.toString text of target
|
||||
* (read without calling an overriding toString) followed by stack frames;
|
||||
* here, as for scriptc's own Error instances, it is the text alone.
|
||||
* Answers undefined, or NULL with the exception pending. */
|
||||
ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target) {
|
||||
/* An instance of a class that extends Error derives its stack from its
|
||||
* name and message when the stack is read. */
|
||||
if (target->kind == SCR_DYN_TYPED_REF) {
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(target);
|
||||
bool error = view && view->kind == SCR_DYN_OBJ && scr_dyn_obj_get(view, "%error", 6) != NULL;
|
||||
scr_dyn_release(view);
|
||||
if (scr_exc_pending()) return NULL;
|
||||
if (error) return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
if (target->kind != SCR_DYN_OBJ) {
|
||||
if (target->kind == SCR_DYN_ARR || target->kind == SCR_DYN_FUNC || target->kind == SCR_DYN_TYPED_REF ||
|
||||
target->kind == SCR_DYN_HANDLE || target->kind == SCR_DYN_PROXY) {
|
||||
static const char message[] = "Error.captureStackTrace on this object is not supported yet";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
} else {
|
||||
static const char message[] = "invalid_argument";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
ScrDynEntry *existing = scr_dyn_find_entry(target, "stack", 5);
|
||||
if (existing ? existing->accessor || !existing->configurable : target->non_extensible) {
|
||||
static const char message[] = "Cannot redefine property: stack";
|
||||
static const char extensible[] = "Cannot define property stack, object is not extensible";
|
||||
if (existing) scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
else scr_throw_error_msg(SCR_ERR_TYPE, extensible, sizeof extensible - 1);
|
||||
return NULL;
|
||||
}
|
||||
ScrStr *text = scr_dyn_error_string(target);
|
||||
if (!text) return NULL;
|
||||
scr_dyn_obj_set_field(target, "stack", 5, scr_dyn_new_str(text));
|
||||
scr_str_release(text);
|
||||
ScrDynEntry *entry = scr_dyn_find_entry(target, "stack", 5);
|
||||
entry->enumerable = false;
|
||||
entry->writable = entry->configurable = true;
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
/* Error.captureStackTrace called through its function value. The
|
||||
* constructor argument's frame cannot be found from a function value, so
|
||||
* every frame is kept: the frames V8 keeps and the ones above them. */
|
||||
static ScrDyn *scr_error_capture_stack_trace_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
(void)closure;
|
||||
return scr_dyn_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined());
|
||||
scr_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined(), NULL, false);
|
||||
return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
/* Error.captureStackTrace read as a value: one function per thread. */
|
||||
static SCR_TL ScrDyn *scr_error_capture_stack_trace_fn;
|
||||
@@ -15430,3 +15391,104 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) {
|
||||
return true;
|
||||
return scr_dyn_object_proto_name(key);
|
||||
}
|
||||
|
||||
/* The `stack` accessor Error.captureStackTrace gives an object that is not
|
||||
* an error. caps[0] holds the captured frames until the first read, then the
|
||||
* stack; caps[1] is nonzero once caps[0] holds the stack. The first read
|
||||
* formats it with the receiver's name and message then, as V8 does. */
|
||||
static ScrDyn *scr_captured_stack_get_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
(void)args; (void)argc;
|
||||
ScrDyn *value = scr_box_get_ref(closure->caps[0]);
|
||||
if (scr_box_get_f64(closure->caps[1]) != 0) return value;
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
ScrStr *heading = scr_dyn_error_string(self);
|
||||
scr_dyn_release(self);
|
||||
if (!heading) {
|
||||
scr_dyn_release(value);
|
||||
return NULL;
|
||||
}
|
||||
ScrStr *stack = scr_str_concat(heading, value->v.str);
|
||||
scr_str_release(heading);
|
||||
scr_dyn_release(value);
|
||||
ScrDyn *out = scr_dyn_new_str(stack);
|
||||
scr_str_release(stack);
|
||||
scr_box_set_ref(closure->caps[0], scr_dyn_retain(out));
|
||||
scr_box_set_f64(closure->caps[1], 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_captured_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
scr_box_set_ref(closure->caps[0], scr_dyn_retain(argc > 0 ? args[0] : scr_dyn_undefined()));
|
||||
scr_box_set_f64(closure->caps[1], 1);
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
/* Error.captureStackTrace(target, constructorOpt), as V8 does it: target.stack
|
||||
* becomes the target's heading and the current stack frames, formatted when
|
||||
* first read. With `skip`, the frames up to and including the topmost one
|
||||
* whose text is `through` are left out (none remain when `through` is NULL or
|
||||
* not on the stack). An error keeps the frames as its own; another object
|
||||
* gets an own non-enumerable, configurable `stack` accessor. A primitive
|
||||
* target throws `TypeError: invalid_argument` and a non-extensible object
|
||||
* V8's define-property TypeError. */
|
||||
void scr_error_capture_stack_trace(ScrDyn *target, const char *through, bool skip) {
|
||||
switch (target->kind) {
|
||||
case SCR_DYN_UNDEF:
|
||||
case SCR_DYN_NULL:
|
||||
case SCR_DYN_BOOL:
|
||||
case SCR_DYN_NUM:
|
||||
case SCR_DYN_STR:
|
||||
case SCR_DYN_BIGINT:
|
||||
case SCR_DYN_SYMBOL: {
|
||||
static const char msg[] = "invalid_argument";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ScrStr *frames = !skip ? scr_stack_capture() : through ? scr_stack_capture_after(through) : NULL;
|
||||
ScrError *error = scr_errdyn_err_of(target);
|
||||
if (error) {
|
||||
scr_str_release(error->stack_frames);
|
||||
error->stack_frames = frames;
|
||||
scr_str_release(error->stack);
|
||||
error->stack = NULL;
|
||||
scr_error_release(error);
|
||||
return;
|
||||
}
|
||||
if (target->kind != SCR_DYN_OBJ && target->kind != SCR_DYN_ARR) {
|
||||
static const char msg[] = "Error.captureStackTrace on this object is not supported yet";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020");
|
||||
scr_str_release(frames);
|
||||
return;
|
||||
}
|
||||
if (!scr_dyn_is_extensible(target)) {
|
||||
static const char msg[] = "Cannot define property stack, object is not extensible";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
scr_str_release(frames);
|
||||
return;
|
||||
}
|
||||
if (!frames) frames = scr_str_new("", 0);
|
||||
/* The getter and setter share their boxes. */
|
||||
ScrClosure *state[2] = {scr_closure_new(NULL, 2), scr_closure_new(NULL, 2)};
|
||||
state[0]->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
||||
state[0]->caps[1] = scr_box_new(SCR_BOX_F64);
|
||||
scr_box_set_ref(state[0]->caps[0], scr_dyn_new_str(frames));
|
||||
scr_str_release(frames);
|
||||
for (int i = 0; i < 2; i++) state[1]->caps[i] = scr_box_retain(state[0]->caps[i]);
|
||||
ScrDyn *descriptor = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(descriptor, "get", 3,
|
||||
scr_dyn_new_func(state[0], scr_captured_stack_get_call, 0, "native:error.stack", NULL));
|
||||
scr_dyn_obj_set(descriptor, "set", 3,
|
||||
scr_dyn_new_func(state[1], scr_captured_stack_set_call, 1, "native:error.stack", NULL));
|
||||
scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false));
|
||||
scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true));
|
||||
ScrStr *key_text = scr_str_new("stack", 5);
|
||||
ScrDyn *key = scr_dyn_new_str(key_text);
|
||||
scr_str_release(key_text);
|
||||
ScrDyn *result = scr_dyn_define_property(target, key, descriptor);
|
||||
scr_dyn_release(result);
|
||||
scr_dyn_release(key);
|
||||
scr_dyn_release(descriptor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Error.captureStackTrace on errors, as Node does it: the error's stack
|
||||
// becomes its heading and the current frames, starting at the function that
|
||||
// called it; a constructor argument leaves out the frames up to and
|
||||
// including that function's topmost call, and every frame when it is not on
|
||||
// the stack. Error subclasses that call it with this.constructor (also
|
||||
// through ?.) keep the frames from the code that ran `new`.
|
||||
function frames(stack: string | undefined): string {
|
||||
const lines = (stack ?? "").split("\n");
|
||||
const wanted = ["inner", "middle", "outer", "makeExpired", "skipped", "caller"];
|
||||
const names = lines.slice(1).map((line) => line.trim().split(" ")[1] ?? "");
|
||||
return `${lines[0]} | ${names.filter((name) => wanted.includes(name)).join(",")}`;
|
||||
}
|
||||
|
||||
function inner(target: Error): Error {
|
||||
Error.captureStackTrace(target);
|
||||
return target;
|
||||
}
|
||||
function middle(target: Error): Error {
|
||||
return inner(target);
|
||||
}
|
||||
function outer(target: Error): Error {
|
||||
return middle(target);
|
||||
}
|
||||
console.log("error", frames(outer(new Error("plain")).stack));
|
||||
console.log("type error", frames(outer(new TypeError("typed")).stack));
|
||||
|
||||
function skipped(target: Error): Error {
|
||||
Error.captureStackTrace(target, skipped);
|
||||
return target;
|
||||
}
|
||||
function caller(target: Error): Error {
|
||||
return skipped(target);
|
||||
}
|
||||
console.log("constructorOpt", frames(caller(new Error("skip")).stack));
|
||||
function unrelated(): void {}
|
||||
const missing = new Error("missing");
|
||||
Error.captureStackTrace(missing, unrelated);
|
||||
console.log("not on the stack", JSON.stringify(missing.stack));
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "AppError";
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
class Expired extends AppError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "Expired";
|
||||
}
|
||||
}
|
||||
class Optional extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "Optional";
|
||||
Error.captureStackTrace?.(this, this.constructor);
|
||||
}
|
||||
}
|
||||
class Named extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
Error.captureStackTrace(this, Named);
|
||||
}
|
||||
}
|
||||
function makeExpired(): Error {
|
||||
return new Expired("token expired");
|
||||
}
|
||||
const expired = makeExpired();
|
||||
console.log("subclass", frames(expired.stack), expired instanceof AppError, expired.message);
|
||||
console.log("optional call", frames(new Optional("maybe").stack));
|
||||
console.log("class name", frames(new Named("named").stack));
|
||||
|
||||
const renamed = new RangeError("range");
|
||||
renamed.name = "Custom";
|
||||
Error.captureStackTrace(renamed);
|
||||
console.log("renamed", frames(renamed.stack));
|
||||
const late = new Error("late");
|
||||
Error.captureStackTrace(late);
|
||||
late.name = "Late";
|
||||
console.log("renamed after", frames(late.stack));
|
||||
|
||||
Error.stackTraceLimit = 1;
|
||||
console.log("limit", frames(outer(new Error("limited")).stack));
|
||||
Error.stackTraceLimit = 0;
|
||||
console.log("limit 0", JSON.stringify(outer(new Error("none")).stack));
|
||||
Error.stackTraceLimit = 10;
|
||||
@@ -0,0 +1,92 @@
|
||||
// Error.captureStackTrace on objects in JavaScript, as Node does it: an own,
|
||||
// non-enumerable, configurable `stack` accessor giving the object's heading
|
||||
// (its name and message when first read, through Error.prototype.toString's
|
||||
// rules, "Error" by default) and the frames from the caller; TypeErrors for a
|
||||
// primitive and a non-extensible target.
|
||||
'use strict';
|
||||
function frames(stack) {
|
||||
const lines = String(stack).split('\n');
|
||||
const names = lines.slice(1).map((line) => line.trim().split(' ')[1]);
|
||||
return `${lines[0]} | ${names.filter((name) => ['inner', 'middle', 'outer', 'skipped', 'caller'].includes(name)).join(',')}`;
|
||||
}
|
||||
function inner(target) {
|
||||
Error.captureStackTrace(target);
|
||||
return target;
|
||||
}
|
||||
function middle(target) {
|
||||
return inner(target);
|
||||
}
|
||||
function outer(target) {
|
||||
return middle(target);
|
||||
}
|
||||
console.log('plain', frames(outer({}).stack));
|
||||
console.log('named', frames(outer({ name: 'Parse', message: 'bad token' }).stack));
|
||||
console.log('message only', frames(outer({ message: 'just this' }).stack));
|
||||
console.log('empty name', frames(outer({ name: '', message: 'no name' }).stack));
|
||||
console.log('number fields', frames(outer({ name: 7, message: 8 }).stack));
|
||||
const own = inner({ kept: 1 });
|
||||
console.log('keys', JSON.stringify(Object.keys(own)), typeof own.stack);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(own, 'stack');
|
||||
console.log('descriptor', typeof descriptor.get, typeof descriptor.set, descriptor.enumerable, descriptor.configurable);
|
||||
own.stack = 'replaced';
|
||||
console.log('writable', own.stack);
|
||||
const later = inner({ message: 'first' });
|
||||
later.name = 'Renamed';
|
||||
later.message = 'second';
|
||||
console.log('formatted when read', frames(later.stack));
|
||||
later.name = 'Again';
|
||||
console.log('formatted once', frames(later.stack));
|
||||
const counted = { message: 'm' };
|
||||
let reads = 0;
|
||||
Object.defineProperty(counted, 'name', { get() { reads++; return 'Counted'; } });
|
||||
inner(counted);
|
||||
console.log('reads before stack', reads);
|
||||
console.log('counted', frames(counted.stack), reads);
|
||||
function skipped(target) {
|
||||
Error.captureStackTrace(target, skipped);
|
||||
return target;
|
||||
}
|
||||
function caller(target) {
|
||||
return skipped(target);
|
||||
}
|
||||
console.log('constructorOpt', frames(caller({}).stack));
|
||||
const arr = [];
|
||||
Error.captureStackTrace(arr);
|
||||
console.log('array', frames(arr.stack), arr.length);
|
||||
for (const value of [5, 'text', undefined, null, true]) {
|
||||
try {
|
||||
Error.captureStackTrace(value);
|
||||
console.log('primitive', String(value), 'no error');
|
||||
} catch (error) {
|
||||
console.log('primitive', String(value), error.name, error.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Error.captureStackTrace(Object.freeze({}));
|
||||
console.log('frozen', 'no error');
|
||||
} catch (error) {
|
||||
console.log('frozen', error.name, error.message);
|
||||
}
|
||||
const api = {
|
||||
run(target) {
|
||||
Error.captureStackTrace(target, api.run);
|
||||
return target;
|
||||
},
|
||||
};
|
||||
function viaApi(target) {
|
||||
return api.run(target);
|
||||
}
|
||||
console.log('member constructorOpt', frames(viaApi({ message: 'member' }).stack).split(' | ')[0]);
|
||||
const captureFn = Error.captureStackTrace;
|
||||
function viaValue(target) {
|
||||
captureFn(target);
|
||||
return target;
|
||||
}
|
||||
const fromValue = viaValue({ message: 'value' });
|
||||
fromValue.message = 'read later';
|
||||
console.log('function value', frames(fromValue.stack), captureFn.name, captureFn.length);
|
||||
try {
|
||||
captureFn(7);
|
||||
} catch (error) {
|
||||
console.log('function value primitive', error.name, error.message);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Error.captureStackTrace gives a stack to errors and untyped objects; a
|
||||
// typed object has no stack property to receive it, and a constructor
|
||||
// argument other than a function declaration or the constructing class
|
||||
// cannot be found on the stack.
|
||||
interface Report {
|
||||
message: string;
|
||||
}
|
||||
const report: Report = { message: "typed" };
|
||||
Error.captureStackTrace(report);
|
||||
|
||||
const arrow = (): void => {};
|
||||
Error.captureStackTrace(new Error("arrow"), arrow);
|
||||
|
||||
class Other {}
|
||||
class Wrapped extends Error {
|
||||
constructor() {
|
||||
super("wrapped");
|
||||
Error.captureStackTrace(this, Other);
|
||||
}
|
||||
}
|
||||
console.log(new Wrapped().stack);
|
||||
@@ -0,0 +1,26 @@
|
||||
error-capture-stack-trace-fences.ts:9:25 - error SC2020: 'Error.captureStackTrace on '{ message: string }' values' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
8 | const report: Report = { message: "typed" };
|
||||
9 | Error.captureStackTrace(report);
|
||||
| ^~~~~~
|
||||
10 |
|
||||
|
||||
hint: pass an Error instance, or an untyped object in JavaScript
|
||||
|
||||
error-capture-stack-trace-fences.ts:12:45 - error SC2020: 'Error.captureStackTrace with this constructor argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
11 | const arrow = (): void => {};
|
||||
12 | Error.captureStackTrace(new Error("arrow"), arrow);
|
||||
| ^~~~~
|
||||
13 |
|
||||
|
||||
hint: pass a function declaration, or this.constructor or the class inside its constructor
|
||||
|
||||
error-capture-stack-trace-fences.ts:18:35 - error SC2020: 'Error.captureStackTrace with this constructor argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
17 | super("wrapped");
|
||||
18 | Error.captureStackTrace(this, Other);
|
||||
| ^~~~~
|
||||
19 | }
|
||||
|
||||
hint: pass a function declaration, or this.constructor or the class inside its constructor
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2446, "san": 2267 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2286, "san": 3360 }
|
||||
Reference in New Issue
Block a user