mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
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.
175 lines
5.6 KiB
JavaScript
175 lines
5.6 KiB
JavaScript
// 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; and a constructorOpt getter or call
|
|
// that runs after the target and before the capture.
|
|
'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);
|
|
}
|
|
const order = [];
|
|
const holder = {
|
|
get ctor() {
|
|
order.push('getter');
|
|
return inner;
|
|
},
|
|
};
|
|
function pick() {
|
|
order.push('call');
|
|
return inner;
|
|
}
|
|
function note(label, value) {
|
|
order.push(label);
|
|
return value;
|
|
}
|
|
Error.captureStackTrace(note('target', {}), holder.ctor);
|
|
Error.captureStackTrace(note('target', {}), pick());
|
|
console.log('constructorOpt evaluated', order.join(','));
|
|
const throwing = {
|
|
get ctor() {
|
|
throw new RangeError('from constructorOpt');
|
|
},
|
|
};
|
|
const untouched = {};
|
|
try {
|
|
Error.captureStackTrace(untouched, throwing.ctor);
|
|
console.log('throwing constructorOpt', 'no error');
|
|
} catch (caught) {
|
|
console.log('throwing constructorOpt', caught.name, caught.message, 'stack' in untouched);
|
|
}
|
|
|
|
// `this.constructor` in a constructor runs a `constructor` getter, and its
|
|
// exception propagates.
|
|
class Watched extends Error {
|
|
constructor() {
|
|
super('watched');
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
get ['constructor']() {
|
|
console.log('constructor getter ran');
|
|
return Watched;
|
|
}
|
|
}
|
|
console.log('getter', typeof new Watched().stack);
|
|
class Throwing extends Error {
|
|
constructor() {
|
|
super('throwing');
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
get ['constructor']() {
|
|
throw new RangeError('constructor getter threw');
|
|
}
|
|
}
|
|
try {
|
|
new Throwing();
|
|
console.log('getter did not throw');
|
|
} 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);
|