mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Capture onto the target evaluated before a constructorOpt that reassigns it
When Error.captureStackTrace evaluates a JavaScript constructorOpt (a getter, a call, or this.constructor with a constructor getter), V8 has already evaluated the target. The lowering kept the target in a local only when its expression was not a plain read, so a constructorOpt that assigned the target variable made the capture install the stack on the new value. The target is now always kept in a local before the constructorOpt runs.
This commit is contained in:
@@ -2,7 +2,7 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { dynUndefinedExpr } from "../../../ir/build.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "../expressions/evaluation-safety.js";
|
||||
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
@@ -541,11 +541,12 @@ export function lowerErrorCaptureStackTraceCall(
|
||||
const stmts: IrStmt[] = [];
|
||||
let receiver = lowerer.coerceToExpected(target, DYN);
|
||||
if (evaluated !== null) {
|
||||
if (!isSafeToRepeat(receiver)) {
|
||||
const local = lowerer.declareHiddenLocal("%captureTarget", DYN);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: receiver, loc });
|
||||
receiver = { kind: "varRef", localId: local.id, type: DYN, loc };
|
||||
}
|
||||
// V8 captures onto the target value it evaluated first. The constructor
|
||||
// argument may assign the variable or field the target was read from, so
|
||||
// even a plain read is kept in a local before the argument runs.
|
||||
const local = lowerer.declareHiddenLocal("%captureTarget", DYN);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: receiver, loc });
|
||||
receiver = { kind: "varRef", localId: local.id, type: DYN, loc };
|
||||
stmts.push({ kind: "exprStmt", expr: evaluated, loc });
|
||||
}
|
||||
const capture: IrExpr = {
|
||||
|
||||
@@ -150,3 +150,25 @@ try {
|
||||
} catch (e) {
|
||||
console.log('getter threw', e instanceof RangeError, e.message);
|
||||
}
|
||||
|
||||
// The stack goes on the target as evaluated before constructorOpt, even when
|
||||
// constructorOpt assigns the variable or field the target was read from.
|
||||
let target = { message: 'original' };
|
||||
const replacement = { message: 'replacement' };
|
||||
const swapping = {
|
||||
get ctor() {
|
||||
target = replacement;
|
||||
return inner;
|
||||
},
|
||||
};
|
||||
const original = target;
|
||||
Error.captureStackTrace(target, swapping.ctor);
|
||||
console.log('reassigned variable', 'stack' in original, 'stack' in replacement, target === replacement);
|
||||
const box = { error: { message: 'boxed' } };
|
||||
const boxed = box.error;
|
||||
function swapField() {
|
||||
box.error = { message: 'swapped in' };
|
||||
return inner;
|
||||
}
|
||||
Error.captureStackTrace(box.error, swapField());
|
||||
console.log('reassigned field', 'stack' in boxed, 'stack' in box.error);
|
||||
|
||||
Reference in New Issue
Block a user