Skip to content

Commit fa9e71e

Browse files
committed
Implement Error.prototype.stack accessor property
This commit implements the Error.prototype.stack property as specified in the TC39 Error Stacks proposal (https://tc39.es/proposal-error-stacks).
1 parent a8fdab1 commit fa9e71e

File tree

6 files changed

+276
-149
lines changed

6 files changed

+276
-149
lines changed

core/engine/src/builtins/error/mod.rs

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//! [spec]: https://tc39.es/ecma262/#sec-error-objects
1111
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
1212
13+
use std::fmt::Write;
14+
1315
use crate::{
1416
Context, JsArgs, JsData, JsResult, JsString, JsValue,
1517
builtins::BuiltInObject,
@@ -20,7 +22,7 @@ use crate::{
2022
property::Attribute,
2123
realm::Realm,
2224
string::StaticJsStrings,
23-
vm::shadow_stack::ShadowEntry,
25+
vm::shadow_stack::ErrorStack,
2426
};
2527
use boa_gc::{Finalize, Trace};
2628
use boa_macros::js_str;
@@ -136,7 +138,7 @@ pub struct Error {
136138

137139
// The position of where the Error was created does not affect equality check.
138140
#[unsafe_ignore_trace]
139-
pub(crate) position: IgnoreEq<Option<ShadowEntry>>,
141+
pub(crate) stack: IgnoreEq<ErrorStack>,
140142
}
141143

142144
impl Error {
@@ -146,34 +148,53 @@ impl Error {
146148
pub fn new(tag: ErrorKind) -> Self {
147149
Self {
148150
tag,
149-
position: IgnoreEq(None),
151+
stack: IgnoreEq(ErrorStack::Position(None)),
150152
}
151153
}
152154

153-
/// Create a new [`Error`] with the given optional [`ShadowEntry`].
154-
pub(crate) fn with_shadow_entry(tag: ErrorKind, entry: Option<ShadowEntry>) -> Self {
155+
/// Create a new [`Error`] with the given [`Stack`].
156+
pub(crate) fn with_stack(tag: ErrorKind, location: ErrorStack) -> Self {
155157
Self {
156158
tag,
157-
position: IgnoreEq(entry),
159+
stack: IgnoreEq(location),
158160
}
159161
}
160162

161163
/// Get the position from the last called function.
162164
pub(crate) fn with_caller_position(tag: ErrorKind, context: &Context) -> Self {
165+
let limit = context.runtime_limits().backtrace_limit();
166+
let backtrace = context.vm.shadow_stack.caller_position(limit);
163167
Self {
164168
tag,
165-
position: IgnoreEq(context.vm.shadow_stack.caller_position()),
169+
stack: IgnoreEq(ErrorStack::Backtrace(backtrace)),
166170
}
167171
}
168172
}
169173

170174
impl IntrinsicObject for Error {
171175
fn init(realm: &Realm) {
172-
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
176+
let property_attribute =
177+
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
178+
let accessor_attribute = Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
179+
180+
let get_stack = BuiltInBuilder::callable(realm, Self::get_stack)
181+
.name(js_string!("get stack"))
182+
.build();
183+
184+
let set_stack = BuiltInBuilder::callable(realm, Self::set_stack)
185+
.name(js_string!("set stack"))
186+
.build();
187+
173188
let builder = BuiltInBuilder::from_standard_constructor::<Self>(realm)
174-
.property(js_string!("name"), Self::NAME, attribute)
175-
.property(js_string!("message"), js_string!(), attribute)
176-
.method(Self::to_string, js_string!("toString"), 0);
189+
.property(js_string!("name"), Self::NAME, property_attribute)
190+
.property(js_string!("message"), js_string!(), property_attribute)
191+
.method(Self::to_string, js_string!("toString"), 0)
192+
.accessor(
193+
js_string!("stack"),
194+
Some(get_stack),
195+
Some(set_stack),
196+
accessor_attribute,
197+
);
177198

178199
#[cfg(feature = "experimental")]
179200
let builder = builder.static_method(Error::is_error, js_string!("isError"), 1);
@@ -192,7 +213,7 @@ impl BuiltInObject for Error {
192213

193214
impl BuiltInConstructor for Error {
194215
const CONSTRUCTOR_ARGUMENTS: usize = 1;
195-
const PROTOTYPE_STORAGE_SLOTS: usize = 3;
216+
const PROTOTYPE_STORAGE_SLOTS: usize = 5;
196217
const CONSTRUCTOR_STORAGE_SLOTS: usize = 1;
197218

198219
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
@@ -263,6 +284,77 @@ impl Error {
263284
Ok(())
264285
}
265286

287+
/// `get Error.prototype.stack`
288+
///
289+
/// The accessor property of Error instances represents the stack trace
290+
/// when the error was created.
291+
///
292+
/// More information:
293+
/// - [Proposal][spec]
294+
///
295+
/// [spec]: https://tc39.es/proposal-error-stacks/
296+
#[allow(clippy::unnecessary_wraps)]
297+
fn get_stack(this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
298+
// 1. Let E be the this value.
299+
// 2. If E is not an Object, return undefined.
300+
let Some(e) = this.as_object() else {
301+
return Ok(JsValue::undefined());
302+
};
303+
304+
// 3. Let errorData be the value of the [[ErrorData]] internal slot of E.
305+
// 4. If errorData is undefined, return undefined.
306+
let Some(error_data) = e.downcast_ref::<Error>() else {
307+
return Ok(JsValue::undefined());
308+
};
309+
310+
// 5. Let stackString be an implementation-defined String value representing the call stack.
311+
// 6. Return stackString.
312+
if let Some(backtrace) = error_data.stack.0.backtrace() {
313+
let stack_string = backtrace
314+
.iter()
315+
.rev()
316+
.fold(String::new(), |mut output, entry| {
317+
let _ = writeln!(&mut output, " at {}", entry.display(true));
318+
output
319+
});
320+
return Ok(js_string!(stack_string).into());
321+
}
322+
323+
// 7. If no stack trace is available, return undefined.
324+
Ok(JsValue::undefined())
325+
}
326+
327+
/// `set Error.prototype.stack`
328+
///
329+
/// The setter for the stack property.
330+
///
331+
/// More information:
332+
/// - [Proposal][spec]
333+
///
334+
/// [spec]: https://tc39.es/proposal-error-stacks/
335+
fn set_stack(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
336+
// 1. Let E be the this value.
337+
// 2. If Type(E) is not Object, throw a TypeError exception.
338+
let e = this.as_object().ok_or_else(|| {
339+
JsNativeError::typ()
340+
.with_message("Error.prototype.stack setter requires that 'this' be an Object")
341+
})?;
342+
343+
// 3. Let numberOfArgs be the number of arguments passed to this function call.
344+
// 4. If numberOfArgs is 0, throw a TypeError exception.
345+
let Some(value) = args.first() else {
346+
return Err(JsNativeError::typ()
347+
.with_message(
348+
"Error.prototype.stack setter requires at least 1 argument, but only 0 were passed",
349+
)
350+
.into());
351+
};
352+
353+
// 5. Return ? CreateDataPropertyOrThrow(E, "stack", value).
354+
e.create_data_property_or_throw(js_string!("stack"), value.clone(), context)
355+
.map(Into::into)
356+
}
357+
266358
/// `Error.prototype.toString()`
267359
///
268360
/// The `toString()` method returns a string representing the specified Error object.

0 commit comments

Comments
 (0)