mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Keep error stacks through computed keys, clones and throwing getters
A read through a computed key now counts as a stack read when the program contains the string "stack", so `e[key]` captures frames like `e.stack` does. A key the program assembles from other strings or from data it reads keeps the static answer. structuredClone of an error held as `any` copies a stack that was already formatted as a non-enumerable data property, as it did before the stack became an accessor. When a name or message getter throws while the stack is formatted, the stack accessor propagates the exception instead of reading the missing string.
This commit is contained in:
@@ -67,6 +67,23 @@ test("a stack read on a checked-dynamic value is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [read("message")])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("a computed key reads the stack when the program spells it", () => {
|
||||
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
|
||||
const key: IrExpr = { loc, kind: "varRef", localId: "k", type: STRING };
|
||||
const computed: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "dynKeyGet", value, key, type: DYN },
|
||||
};
|
||||
const spelled: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "strLit", value: "stack", type: STRING },
|
||||
};
|
||||
expect(observesStacks(mod([fn("%main", [computed, spelled])]))).toBe(true);
|
||||
expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -13,15 +13,34 @@ 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 {
|
||||
/** A typed `error.stack` read, or a read on a checked-dynamic value that
|
||||
* can reach an error's own `stack` accessor: the constant key `stack`, or
|
||||
* a computed key when `computedKeys` (the program spells "stack", so a
|
||||
* computed key can name it). A key the program assembles from other
|
||||
* strings or from data it reads keeps the static answer. */
|
||||
function readsStack(expr: IrExpr, computedKeys: boolean): boolean {
|
||||
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
|
||||
return expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && expr.key.value === "stack";
|
||||
if (expr.kind !== "dynKeyGet") return false;
|
||||
if (expr.key.kind === "strLit") return expr.key.value === "stack";
|
||||
return computedKeys;
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) });
|
||||
/** Whether the module contains the string literal "stack". */
|
||||
function spellsStack(mod: IrModule): boolean {
|
||||
return mod.functions.some(
|
||||
(fn) =>
|
||||
!everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !(expr.kind === "strLit" && expr.value === "stack"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean {
|
||||
return !everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !readsStack(expr, computedKeys),
|
||||
});
|
||||
}
|
||||
|
||||
/** Visit every expression of `body` that execution can reach: statements
|
||||
@@ -46,7 +65,8 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
if (!mod.functions.some(containsStackRead)) return false;
|
||||
const computedKeys = spellsStack(mod);
|
||||
if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
@@ -70,7 +90,7 @@ export function observesStacks(mod: IrModule): boolean {
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr)) observed = true;
|
||||
if (readsStack(expr, computedKeys)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
|
||||
@@ -6754,6 +6754,8 @@ static ScrDyn *scr_error_stack_get_call(ScrClosure *closure, ScrDyn *const *args
|
||||
if (!error) return scr_dyn_retain(scr_dyn_undefined());
|
||||
ScrStr *stack = scr_error_stack(error);
|
||||
scr_error_release(error);
|
||||
/* A name or message getter that throws leaves its exception pending. */
|
||||
if (!stack) return NULL;
|
||||
ScrDyn *out = scr_dyn_new_str(stack);
|
||||
scr_str_release(stack);
|
||||
return out;
|
||||
@@ -10954,7 +10956,17 @@ 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;
|
||||
if (scr_dyn_entry_is_error_stack(e)) {
|
||||
/* A stack that was already formatted copies as a non-enumerable
|
||||
* data property; an unformatted one has nothing to copy. */
|
||||
ScrError *error = scr_errdyn_err_of(v);
|
||||
if (error && error->stack) {
|
||||
scr_dyn_obj_set(out, "stack", 5, scr_dyn_new_str(error->stack));
|
||||
out->v.obj.entries[out->v.obj.len - 1].enumerable = false;
|
||||
}
|
||||
scr_error_release(error);
|
||||
continue;
|
||||
}
|
||||
ScrDyn *c = scr_sc_clone(e->value, &self, region);
|
||||
if (c == NULL) {
|
||||
scr_dyn_release(out);
|
||||
|
||||
@@ -124,3 +124,24 @@ function makeSubclass(): any {
|
||||
return dyn(new PlainSubclass("deep"));
|
||||
}
|
||||
console.log("subclass frames", (makeSubclass().stack as string).includes("at makeSubclass"));
|
||||
|
||||
const formatted = dyn(new Error("formatted"));
|
||||
console.log("formatted before clone", firstLine(formatted.stack));
|
||||
console.log("clone keeps a formatted stack", firstLine(dyn(structuredClone(formatted)).stack));
|
||||
|
||||
class GrumpyError extends Error {
|
||||
override get name(): string {
|
||||
throw new Error("no name today");
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log("throwing name getter", firstLine(dyn(new GrumpyError("grumpy")).stack));
|
||||
} catch (error) {
|
||||
console.log("throwing name getter threw", (error as Error).message);
|
||||
}
|
||||
|
||||
function readKey(target: any, key: string): unknown {
|
||||
return target[key];
|
||||
}
|
||||
const computed = readKey(inner(), ["sta", "ck"].join("")) as string;
|
||||
console.log("computed key", firstLine(computed), computed.includes("at inner"));
|
||||
|
||||
@@ -124,3 +124,25 @@ function makeSubclass() {
|
||||
return dyn(new PlainSubclass("deep"));
|
||||
}
|
||||
console.log("subclass frames", makeSubclass().stack.includes("at makeSubclass"));
|
||||
|
||||
const formatted = dyn(new Error("formatted"));
|
||||
console.log("formatted before clone", firstLine(formatted.stack));
|
||||
console.log("clone keeps a formatted stack", firstLine(dyn(structuredClone(formatted)).stack));
|
||||
|
||||
class GrumpyError extends Error {
|
||||
/** @returns {string} */
|
||||
get name() {
|
||||
throw new Error("no name today");
|
||||
}
|
||||
}
|
||||
try {
|
||||
console.log("throwing name getter", firstLine(dyn(new GrumpyError("grumpy")).stack));
|
||||
} catch (error) {
|
||||
console.log("throwing name getter threw", error.message);
|
||||
}
|
||||
|
||||
function readKey(target, key) {
|
||||
return target[key];
|
||||
}
|
||||
const computed = readKey(inner(), ["sta", "ck"].join(""));
|
||||
console.log("computed key", firstLine(computed), computed.includes("at inner"));
|
||||
|
||||
Reference in New Issue
Block a user