mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Give errors held as any their own stack accessor
In Node every Error has an own, non-enumerable `stack` accessor, shared by all errors, that formats the stack on first access. Errors that reached checked-dynamic code read `stack` as undefined: the error encoding carried a stack only when a typed read had already formatted one. Reads on instances of Error subclasses found no class arm for it, and assigning `stack` on a subclass instance, typed or through `any`, did not change what a typed read answered. The encoding now lists `stack` first among its own properties as a non-enumerable accessor. Its getter formats the native error's stack on first access, so the heading is the name and message at that point, and its setter stores a string in the native error, which typed reads share. Any other value replaces the accessor with a data property. Objects that inherit from an error read the error's stack, and a getter called on any other receiver answers undefined. A `stack` read on a checked-dynamic value counts as a stack observation, so such programs capture frames as they do for typed reads. Dynamic reads and writes of `stack` on Error subclass instances use the native stack; non-string values are kept with the instance's own properties.
This commit is contained in:
@@ -827,6 +827,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.sameValue": "scr_dyn_same_value",
|
||||
"error.toString": "scr_error_to_string",
|
||||
"error.stack": "scr_error_stack",
|
||||
"error.setStack": "scr_error_set_stack",
|
||||
"error.stackLimitGet": "scr_stack_limit_get",
|
||||
"error.stackLimitSet": "scr_stack_limit_set",
|
||||
"class.name": "scr_classobj_name",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
@@ -49,6 +50,23 @@ test("a reachable stack read is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
|
||||
});
|
||||
|
||||
test("a stack read on a checked-dynamic value is observed", () => {
|
||||
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
|
||||
const read = (key: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "dynKeyGet",
|
||||
value,
|
||||
key: { loc, kind: "strLit", value: key, type: STRING },
|
||||
type: DYN,
|
||||
},
|
||||
});
|
||||
expect(observesStacks(mod([fn("%main", [read("stack")])]))).toBe(true);
|
||||
expect(observesStacks(mod([fn("%main", [read("message")])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -13,8 +13,11 @@ import { writesPublished } from "./publication.js";
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
/** A typed `error.stack` read, or a `stack` read on a checked-dynamic
|
||||
* value, which reaches an error's own `stack` accessor. */
|
||||
function readsStack(expr: IrExpr): boolean {
|
||||
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
|
||||
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
|
||||
return expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && expr.key.value === "stack";
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
|
||||
@@ -2694,6 +2694,7 @@ export class ClassDynamicDispatch {
|
||||
!findGenericMethodOn(lowerer, info, dispatch.name) &&
|
||||
dispatch.name !== "constructor" &&
|
||||
!info.fields.has(dispatch.name) &&
|
||||
!(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) &&
|
||||
!findMethodOn(lowerer, info, `get:${dispatch.name}`) &&
|
||||
!findMethodOn(lowerer, info, `set:${dispatch.name}`))
|
||||
)
|
||||
@@ -4498,6 +4499,110 @@ export class ClassDynamicDispatch {
|
||||
},
|
||||
];
|
||||
}
|
||||
// An Error's own `stack`: a string is the native error's stack, which
|
||||
// typed reads share; any other value is kept with the instance's own
|
||||
// properties, which a read checks first. A class whose chain declares
|
||||
// a `stack` accessor answers through it (the accessor code below).
|
||||
if (
|
||||
name === "stack" &&
|
||||
!info.fields.has(name) &&
|
||||
lowerer.isSubclassOf(info.def.name, "%Error") &&
|
||||
!findMethodOn(lowerer, info, "get:stack") &&
|
||||
!findMethodOn(lowerer, info, "set:stack")
|
||||
) {
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const error = lowerer.upcastTo(receiver, "%Error");
|
||||
if (write) {
|
||||
const helper = classPropertiesHelper(lowerer, loc);
|
||||
this.generated.add(helper);
|
||||
const store: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [
|
||||
{
|
||||
kind: "call",
|
||||
callee: helper.name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
key,
|
||||
varRef("p.1", DYN, loc),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "dynTest",
|
||||
test: "string",
|
||||
value: varRef("p.1", DYN, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "error.setStack",
|
||||
args: [error, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: this.readBagHas(
|
||||
lowerer,
|
||||
"dyn.hasKey",
|
||||
this.readPropertyBag(lowerer, receiver, loc),
|
||||
key,
|
||||
loc,
|
||||
),
|
||||
then: [store],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: [store],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
}
|
||||
const bag = this.readPropertyBag(lowerer, receiver, loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bag, key, lowerer.coerceToExpected(receiver, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: lowerer.coerceToExpected(
|
||||
{ kind: "libCall", fn: "error.stack", args: [error], type: STRING, loc },
|
||||
DYN,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error"))
|
||||
return write
|
||||
? [
|
||||
|
||||
@@ -3231,6 +3231,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** SameValue over checked values, including NaN, signed zero and reference identity. */
|
||||
"dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL },
|
||||
"error.stack": { argTypes: [null], result: STRING },
|
||||
"error.setStack": { argTypes: [null, STRING], result: VOID },
|
||||
"error.stackLimitGet": { argTypes: [], result: F64 },
|
||||
"error.stackLimitSet": { argTypes: [F64], result: VOID },
|
||||
// Receiver (any error-hierarchy object) and the program-dependent
|
||||
|
||||
@@ -4109,6 +4109,7 @@ function validateFunction(
|
||||
if (checkLibEmitterCall(e, sig.result)) return;
|
||||
if (
|
||||
e.fn === "error.stack" ||
|
||||
e.fn === "error.setStack" ||
|
||||
e.fn === "error.cause" ||
|
||||
e.fn === "error.hasCause" ||
|
||||
e.fn === "error.setCause" ||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5916-dyn-error-stack-property.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6400-dyn-error-stack-property-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -354,6 +354,12 @@ ScrStr *scr_error_stack(ScrError *e) {
|
||||
return scr_str_retain(e->stack);
|
||||
}
|
||||
|
||||
void scr_error_set_stack(ScrError *e, ScrStr *stack) {
|
||||
ScrStr *replacement = scr_str_retain(stack);
|
||||
scr_str_release(e->stack);
|
||||
e->stack = replacement;
|
||||
}
|
||||
|
||||
bool scr_error_is(const void *obj) {
|
||||
const ScrVt *vt = ((const ScrError *)obj)->vt;
|
||||
return scr_error_vts[SCR_ERR_ERROR].pre <= vt->pre &&
|
||||
|
||||
@@ -252,6 +252,9 @@ void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *));
|
||||
void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *));
|
||||
ScrStr *scr_error_default_name(const ScrError *e);
|
||||
ScrStr *scr_error_stack(ScrError *e);
|
||||
/* Replaces the error's stack string, as an assignment to `stack` does.
|
||||
* Borrowed operands. */
|
||||
void scr_error_set_stack(ScrError *e, ScrStr *stack);
|
||||
|
||||
ScrError *scr_error_retain(ScrError *e);
|
||||
void scr_error_release(ScrError *e); /* dispatches through e->vt */
|
||||
|
||||
@@ -6719,10 +6719,109 @@ static void scr_errdyn_teardown(void) {
|
||||
free(cache);
|
||||
}
|
||||
|
||||
/* An error's own `stack`: a non-enumerable accessor shared by every error,
|
||||
* as V8 defines it. The getter formats the stack on first access, so its
|
||||
* heading is the name and message at that point, and later reads answer
|
||||
* the same string. Both functions find the error through the receiver or
|
||||
* its prototype chain; on any other receiver the getter answers undefined
|
||||
* and the setter changes nothing but the receiver's own accessor. */
|
||||
static SCR_TL ScrDyn *scr_error_stack_accessors[2];
|
||||
static SCR_TL bool scr_error_stack_accessors_registered;
|
||||
|
||||
static void scr_error_stack_accessors_cleanup(void) {
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
scr_dyn_release(scr_error_stack_accessors[i]);
|
||||
scr_error_stack_accessors[i] = NULL;
|
||||
}
|
||||
scr_error_stack_accessors_registered = false;
|
||||
}
|
||||
|
||||
/* The native error behind an accessor receiver. +1 or NULL. */
|
||||
static ScrError *scr_error_stack_holder(const ScrDyn *receiver) {
|
||||
for (const ScrDyn *current = receiver; current; current = scr_dyn_proto_next(current)) {
|
||||
ScrError *error = scr_errdyn_err_of(current);
|
||||
if (error) return error;
|
||||
if (current->kind != SCR_DYN_OBJ) return NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
(void)closure; (void)args; (void)argc;
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
ScrError *error = scr_error_stack_holder(self);
|
||||
scr_dyn_release(self);
|
||||
if (!error) return scr_dyn_retain(scr_dyn_undefined());
|
||||
ScrStr *stack = scr_error_stack(error);
|
||||
scr_error_release(error);
|
||||
ScrDyn *out = scr_dyn_new_str(stack);
|
||||
scr_str_release(stack);
|
||||
return out;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc);
|
||||
|
||||
/* Whether an own property is an error's `stack` accessor. */
|
||||
static bool scr_dyn_entry_is_error_stack(const ScrDynEntry *entry) {
|
||||
return entry->accessor && entry->setter && entry->setter->kind == SCR_DYN_FUNC &&
|
||||
entry->setter->v.fn.thunk == scr_error_stack_set_call;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_error_stack_set_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
(void)closure;
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
ScrError *error = scr_error_stack_holder(self);
|
||||
ScrDyn *value = argc > 0 ? args[0] : scr_dyn_undefined();
|
||||
if (error && value->kind == SCR_DYN_STR) {
|
||||
scr_error_set_stack(error, value->v.str);
|
||||
} else if (self->kind == SCR_DYN_OBJ) {
|
||||
/* The native slot holds strings only, and a typed reference's
|
||||
* snapshot names no error: the receiver's own accessor becomes a data
|
||||
* property holding the value, which a snapshot's commit stores. */
|
||||
ScrStr *key = scr_str_new("stack", 5);
|
||||
ScrDynEntry *entry = scr_dyn_entry(self, key);
|
||||
scr_str_release(key);
|
||||
if (entry && scr_dyn_entry_is_error_stack(entry)) {
|
||||
ScrDyn *old_value = entry->value, *old_getter = entry->getter, *old_setter = entry->setter;
|
||||
entry->value = scr_dyn_retain(value);
|
||||
entry->getter = entry->setter = NULL;
|
||||
entry->accessor = false;
|
||||
entry->writable = true;
|
||||
scr_dyn_release(old_value);
|
||||
scr_dyn_release(old_getter);
|
||||
scr_dyn_release(old_setter);
|
||||
}
|
||||
}
|
||||
scr_error_release(error);
|
||||
scr_dyn_release(self);
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
static void scr_error_dyn_add_stack(ScrDyn *d) {
|
||||
if (!scr_error_stack_accessors_registered) {
|
||||
scr_error_stack_accessors_registered = true;
|
||||
scr_atexit(scr_error_stack_accessors_cleanup);
|
||||
}
|
||||
if (!scr_error_stack_accessors[0]) {
|
||||
scr_error_stack_accessors[0] =
|
||||
scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_get_call, 0, "native:error.stack", NULL);
|
||||
scr_error_stack_accessors[1] =
|
||||
scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_stack_set_call, 1, "native:error.stack", NULL);
|
||||
}
|
||||
scr_dyn_obj_set(d, "stack", 5, scr_dyn_retain(scr_dyn_undefined()));
|
||||
ScrDynEntry *entry = &d->v.obj.entries[d->v.obj.len - 1];
|
||||
entry->accessor = true;
|
||||
entry->getter = scr_dyn_retain(scr_error_stack_accessors[0]);
|
||||
entry->setter = scr_dyn_retain(scr_error_stack_accessors[1]);
|
||||
entry->enumerable = false;
|
||||
}
|
||||
|
||||
ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
ScrDyn *d = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
/* Node lists `stack` first among an error's own properties. */
|
||||
scr_error_dyn_add_stack(d);
|
||||
if (e->name_present) {
|
||||
scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable;
|
||||
@@ -6744,11 +6843,6 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
|
||||
scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable;
|
||||
}
|
||||
if (e->stack) {
|
||||
/* Node's own `stack` is non-enumerable. */
|
||||
scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack));
|
||||
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
|
||||
}
|
||||
if (e->system_message) {
|
||||
/* SystemError: code, the read-only `info` context ({ message, path,
|
||||
* syscall, errno, code }), then errno, syscall, path and dest. */
|
||||
@@ -10860,6 +10954,7 @@ static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion
|
||||
ScrDyn *out = scr_dyn_new_obj();
|
||||
for (size_t i = 0; i < v->v.obj.len; i++) {
|
||||
const ScrDynEntry *e = &v->v.obj.entries[i];
|
||||
if (scr_dyn_entry_is_error_stack(e)) continue;
|
||||
ScrDyn *c = scr_sc_clone(e->value, &self, region);
|
||||
if (c == NULL) {
|
||||
scr_dyn_release(out);
|
||||
|
||||
@@ -801,6 +801,12 @@ ScrDyn *scr_message_decode(const ScrMessage *message) {
|
||||
}
|
||||
if (!values[i]->v.arr.properties) values[i]->v.arr.properties = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(values[i]->v.arr.properties, property->key, property->length, value);
|
||||
} else if (node->kind == SCR_MESSAGE_ERROR && property->length == 5 && !memcmp(property->key, "stack", 5)) {
|
||||
/* The new error's own `stack` accessor stores the value. */
|
||||
ScrStr *key = scr_str_new(property->key, property->length);
|
||||
scr_dyn_key_set(values[i], key, value);
|
||||
scr_str_release(key);
|
||||
scr_dyn_release(value);
|
||||
} else scr_dyn_obj_set(values[i], property->key, property->length, value);
|
||||
}
|
||||
if (node->kind == SCR_MESSAGE_ERROR) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// Errors held as `any` or `unknown` have an own `stack` like Node's: a
|
||||
// non-enumerable accessor that every error shares. The first read formats
|
||||
// "Name: message" from the error's name and message at that point, and
|
||||
// later reads answer the same string. Assigning a value replaces it,
|
||||
// `delete` removes it, Object.keys, spread, Object.assign and
|
||||
// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects
|
||||
// that inherit from an error read the error's stack. Typed and dynamic
|
||||
// reads answer the same string, and a stack read through `any` captures
|
||||
// frames. Only the first line is printed: frame lines differ between
|
||||
// runtimes.
|
||||
|
||||
class ParseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ParseError";
|
||||
}
|
||||
}
|
||||
|
||||
class PlainSubclass extends Error {}
|
||||
|
||||
function dyn(value: unknown): any {
|
||||
return value;
|
||||
}
|
||||
|
||||
function firstLine(value: unknown): string {
|
||||
return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`;
|
||||
}
|
||||
|
||||
function describe(label: string, error: any): void {
|
||||
console.log(
|
||||
label,
|
||||
"|",
|
||||
typeof error.stack,
|
||||
firstLine(error.stack),
|
||||
"|",
|
||||
Object.keys(error).join(","),
|
||||
JSON.stringify(error),
|
||||
"|",
|
||||
"stack" in error,
|
||||
Object.hasOwn(error, "stack"),
|
||||
error.propertyIsEnumerable("stack"),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
throw new RangeError("thrown");
|
||||
} catch (error) {
|
||||
describe("caught", error);
|
||||
}
|
||||
|
||||
const unknownError: unknown = new TypeError("bad thing");
|
||||
describe("unknown", unknownError);
|
||||
describe("plain", dyn(new Error("plain")));
|
||||
describe("no message", dyn(new SyntaxError()));
|
||||
describe("subclass", dyn(new ParseError("line 3")));
|
||||
describe("plain subclass", dyn(new PlainSubclass("inherited name")));
|
||||
|
||||
const coded = dyn(new Error("with code"));
|
||||
coded.code = "E_CODE";
|
||||
describe("own property", coded);
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack")!;
|
||||
const other = Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack")!;
|
||||
console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable);
|
||||
console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set);
|
||||
console.log("foreign receiver", descriptor.get!.call({}), firstLine(descriptor.get!.call(dyn(new TypeError("b")))));
|
||||
const subclassDescriptor = Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack")!;
|
||||
console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get);
|
||||
|
||||
const renamed = dyn(new Error("before"));
|
||||
renamed.message = "after";
|
||||
renamed.name = "Renamed";
|
||||
console.log("first read", firstLine(renamed.stack));
|
||||
renamed.message = "later";
|
||||
console.log("later read", firstLine(renamed.stack));
|
||||
const subclassRenamed = dyn(new PlainSubclass("before"));
|
||||
subclassRenamed.message = "after";
|
||||
console.log("subclass first read", firstLine(subclassRenamed.stack));
|
||||
|
||||
const replaced = dyn(new Error("replaced"));
|
||||
replaced.stack = "custom stack\n at nowhere";
|
||||
describe("assigned", replaced);
|
||||
console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")!).join(","));
|
||||
const subclassReplaced = dyn(new ParseError("replaced"));
|
||||
subclassReplaced.stack = "subclass stack";
|
||||
console.log("subclass assigned", subclassReplaced.stack);
|
||||
const typedReplaced = new ParseError("typed write");
|
||||
typedReplaced.stack = "typed subclass stack";
|
||||
console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack);
|
||||
const subclassNumbered = dyn(new PlainSubclass("numbered"));
|
||||
subclassNumbered.stack = 7;
|
||||
console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack);
|
||||
subclassNumbered.stack = "string again";
|
||||
console.log("subclass string again", subclassNumbered.stack);
|
||||
const numbered = dyn(new Error("numbered"));
|
||||
numbered.stack = 5;
|
||||
console.log("number", numbered.stack, typeof numbered.stack);
|
||||
const removed = dyn(new Error("removed"));
|
||||
delete removed.stack;
|
||||
console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack"));
|
||||
|
||||
const spreadSource = dyn(new Error("spread"));
|
||||
console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length);
|
||||
console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack));
|
||||
|
||||
const typed = new Error("typed");
|
||||
const view = dyn(typed);
|
||||
console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack));
|
||||
const typedSubclass = new ParseError("typed subclass");
|
||||
console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack);
|
||||
const assignedThroughView = new Error("typed");
|
||||
dyn(assignedThroughView).stack = "through the view";
|
||||
console.log("assigned through view", assignedThroughView.stack);
|
||||
|
||||
function inner(): any {
|
||||
return dyn(new Error("deep"));
|
||||
}
|
||||
function outer(): any {
|
||||
return inner();
|
||||
}
|
||||
const deep: string = outer().stack;
|
||||
console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer"));
|
||||
function makeSubclass(): any {
|
||||
return dyn(new PlainSubclass("deep"));
|
||||
}
|
||||
console.log("subclass frames", (makeSubclass().stack as string).includes("at makeSubclass"));
|
||||
@@ -0,0 +1,126 @@
|
||||
// In JavaScript sources, errors have an own `stack` like Node's: a
|
||||
// non-enumerable accessor that every error shares. The first read formats
|
||||
// "Name: message" from the error's name and message at that point, and
|
||||
// later reads answer the same string. Assigning a value replaces it,
|
||||
// `delete` removes it, Object.keys, spread, Object.assign and
|
||||
// JSON.stringify leave it out, `in` and Object.hasOwn find it, objects
|
||||
// that inherit from an error read the error's stack. Typed and dynamic
|
||||
// reads answer the same string, and a stack read through `any` captures
|
||||
// frames. Only the first line is printed: frame lines differ between
|
||||
// runtimes.
|
||||
|
||||
class ParseError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ParseError";
|
||||
}
|
||||
}
|
||||
|
||||
class PlainSubclass extends Error {}
|
||||
|
||||
function dyn(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function firstLine(value) {
|
||||
return typeof value === "string" ? JSON.stringify(value.split("\n")[0]) : `(${typeof value} ${String(value)})`;
|
||||
}
|
||||
|
||||
function describe(label, error) {
|
||||
console.log(
|
||||
label,
|
||||
"|",
|
||||
typeof error.stack,
|
||||
firstLine(error.stack),
|
||||
"|",
|
||||
Object.keys(error).join(","),
|
||||
JSON.stringify(error),
|
||||
"|",
|
||||
"stack" in error,
|
||||
Object.hasOwn(error, "stack"),
|
||||
error.propertyIsEnumerable("stack"),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
throw new RangeError("thrown");
|
||||
} catch (error) {
|
||||
describe("caught", error);
|
||||
}
|
||||
|
||||
const unknownError = new TypeError("bad thing");
|
||||
describe("unknown", unknownError);
|
||||
describe("plain", dyn(new Error("plain")));
|
||||
describe("no message", dyn(new SyntaxError()));
|
||||
describe("subclass", dyn(new ParseError("line 3")));
|
||||
describe("plain subclass", dyn(new PlainSubclass("inherited name")));
|
||||
|
||||
const coded = dyn(new Error("with code"));
|
||||
coded.code = "E_CODE";
|
||||
describe("own property", coded);
|
||||
|
||||
const descriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new Error("a")), "stack"));
|
||||
const other = dyn(Object.getOwnPropertyDescriptor(dyn(new TypeError("b")), "stack"));
|
||||
console.log("descriptor", Object.keys(descriptor).join(","), descriptor.enumerable, descriptor.configurable);
|
||||
console.log("shared accessors", typeof descriptor.get, descriptor.get === other.get, descriptor.set === other.set);
|
||||
console.log("foreign receiver", descriptor.get.call({}), firstLine(descriptor.get.call(dyn(new TypeError("b")))));
|
||||
const subclassDescriptor = dyn(Object.getOwnPropertyDescriptor(dyn(new PlainSubclass("s")), "stack"));
|
||||
console.log("subclass descriptor", Object.keys(subclassDescriptor).join(","), subclassDescriptor.get === descriptor.get);
|
||||
|
||||
const renamed = dyn(new Error("before"));
|
||||
renamed.message = "after";
|
||||
renamed.name = "Renamed";
|
||||
console.log("first read", firstLine(renamed.stack));
|
||||
renamed.message = "later";
|
||||
console.log("later read", firstLine(renamed.stack));
|
||||
const subclassRenamed = dyn(new PlainSubclass("before"));
|
||||
subclassRenamed.message = "after";
|
||||
console.log("subclass first read", firstLine(subclassRenamed.stack));
|
||||
|
||||
const replaced = dyn(new Error("replaced"));
|
||||
replaced.stack = "custom stack\n at nowhere";
|
||||
describe("assigned", replaced);
|
||||
console.log("assigned descriptor", Object.keys(Object.getOwnPropertyDescriptor(replaced, "stack")).join(","));
|
||||
const subclassReplaced = dyn(new ParseError("replaced"));
|
||||
subclassReplaced.stack = "subclass stack";
|
||||
console.log("subclass assigned", subclassReplaced.stack);
|
||||
const typedReplaced = new ParseError("typed write");
|
||||
typedReplaced.stack = "typed subclass stack";
|
||||
console.log("typed subclass assigned", typedReplaced.stack, dyn(typedReplaced).stack);
|
||||
const subclassNumbered = dyn(new PlainSubclass("numbered"));
|
||||
subclassNumbered.stack = 7;
|
||||
console.log("subclass number", subclassNumbered.stack, typeof subclassNumbered.stack);
|
||||
subclassNumbered.stack = "string again";
|
||||
console.log("subclass string again", subclassNumbered.stack);
|
||||
const numbered = dyn(new Error("numbered"));
|
||||
numbered.stack = 5;
|
||||
console.log("number", numbered.stack, typeof numbered.stack);
|
||||
const removed = dyn(new Error("removed"));
|
||||
delete removed.stack;
|
||||
console.log("deleted", removed.stack, "stack" in removed, Object.hasOwn(removed, "stack"));
|
||||
|
||||
const spreadSource = dyn(new Error("spread"));
|
||||
console.log("spread", Object.keys({ ...spreadSource }).length, Object.keys(Object.assign({}, spreadSource)).length);
|
||||
console.log("inherited", firstLine(Object.create(dyn(new RangeError("prototype"))).stack));
|
||||
|
||||
const typed = new Error("typed");
|
||||
const view = dyn(typed);
|
||||
console.log("typed and dynamic", view.stack === typed.stack, firstLine(typed.stack));
|
||||
const typedSubclass = new ParseError("typed subclass");
|
||||
console.log("typed subclass", firstLine(dyn(typedSubclass).stack), dyn(typedSubclass).stack === typedSubclass.stack);
|
||||
const assignedThroughView = new Error("typed");
|
||||
dyn(assignedThroughView).stack = "through the view";
|
||||
console.log("assigned through view", assignedThroughView.stack);
|
||||
|
||||
function inner() {
|
||||
return dyn(new Error("deep"));
|
||||
}
|
||||
function outer() {
|
||||
return inner();
|
||||
}
|
||||
const deep = outer().stack;
|
||||
console.log("frames", deep.split("\n").length > 1, deep.includes("at inner"), deep.includes("at outer"));
|
||||
function makeSubclass() {
|
||||
return dyn(new PlainSubclass("deep"));
|
||||
}
|
||||
console.log("subclass frames", makeSubclass().stack.includes("at makeSubclass"));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3232, "san": 4298 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3172, "san": 4044 }
|
||||
Reference in New Issue
Block a user