Skip to content

Commit 301a7d8

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 e654d11 commit 301a7d8

File tree

5 files changed

+289
-141
lines changed

5 files changed

+289
-141
lines changed

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

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ use crate::{
2020
property::Attribute,
2121
realm::Realm,
2222
string::StaticJsStrings,
23-
vm::shadow_stack::ShadowEntry,
23+
vm::shadow_stack::{ErrorLocation, ShadowEntry},
24+
vm::source_info::NativeSourceInfo,
2425
};
2526
use boa_gc::{Finalize, Trace};
2627
use boa_macros::js_str;
@@ -136,44 +137,67 @@ pub struct Error {
136137

137138
// The position of where the Error was created does not affect equality check.
138139
#[unsafe_ignore_trace]
139-
pub(crate) position: IgnoreEq<Option<ShadowEntry>>,
140+
pub(crate) location: IgnoreEq<ErrorLocation>,
140141
}
141142

142143
impl Error {
143144
/// Create a new [`Error`].
144145
#[inline]
145146
#[must_use]
147+
#[cfg_attr(feature = "native-backtrace", track_caller)]
146148
pub fn new(tag: ErrorKind) -> Self {
147149
Self {
148150
tag,
149-
position: IgnoreEq(None),
151+
location: IgnoreEq::new(ErrorLocation::Position(ShadowEntry::Native {
152+
function_name: None,
153+
source_info: NativeSourceInfo::caller(),
154+
})),
150155
}
151156
}
152157

153-
/// Create a new [`Error`] with the given optional [`ShadowEntry`].
154-
pub(crate) fn with_shadow_entry(tag: ErrorKind, entry: Option<ShadowEntry>) -> Self {
158+
/// Create a new [`Error`] with the given [`ErrorLocation`].
159+
pub(crate) fn with_location(tag: ErrorKind, location: ErrorLocation) -> Self {
155160
Self {
156161
tag,
157-
position: IgnoreEq(entry),
162+
location: IgnoreEq::new(location),
158163
}
159164
}
160165

161166
/// Get the position from the last called function.
162167
pub(crate) fn with_caller_position(tag: ErrorKind, context: &Context) -> Self {
168+
let limit = context.runtime_limits().backtrace_limit();
169+
let backtrace = context.vm.shadow_stack.caller_position(limit);
163170
Self {
164171
tag,
165-
position: IgnoreEq(context.vm.shadow_stack.caller_position()),
172+
location: IgnoreEq::new(ErrorLocation::Backtrace(backtrace)),
166173
}
167174
}
168175
}
169176

170177
impl IntrinsicObject for Error {
171178
fn init(realm: &Realm) {
172-
let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
179+
let property_attribute =
180+
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
181+
let accessor_attribute = Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
182+
183+
let get_stack = BuiltInBuilder::callable(realm, Self::get_stack)
184+
.name(js_string!("get stack"))
185+
.build();
186+
187+
let set_stack = BuiltInBuilder::callable(realm, Self::set_stack)
188+
.name(js_string!("set stack"))
189+
.build();
190+
173191
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);
192+
.property(js_string!("name"), Self::NAME, property_attribute)
193+
.property(js_string!("message"), js_string!(), property_attribute)
194+
.method(Self::to_string, js_string!("toString"), 0)
195+
.accessor(
196+
js_string!("stack"),
197+
Some(get_stack),
198+
Some(set_stack),
199+
accessor_attribute,
200+
);
177201

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

193217
impl BuiltInConstructor for Error {
194218
const CONSTRUCTOR_ARGUMENTS: usize = 1;
195-
const PROTOTYPE_STORAGE_SLOTS: usize = 3;
219+
const PROTOTYPE_STORAGE_SLOTS: usize = 5;
196220
const CONSTRUCTOR_STORAGE_SLOTS: usize = 1;
197221

198222
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
@@ -263,6 +287,76 @@ impl Error {
263287
Ok(())
264288
}
265289

290+
/// `get Error.prototype.stack`
291+
///
292+
/// The accessor property of Error instances represents the stack trace
293+
/// when the error was created.
294+
///
295+
/// More information:
296+
/// - [Proposal][spec]
297+
///
298+
/// [spec]: https://tc39.es/proposal-error-stacks/
299+
fn get_stack(this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
300+
// 1. Let E be the this value.
301+
// 2. If E is not an Object, return undefined.
302+
let Some(e) = this.as_object() else {
303+
return Ok(JsValue::undefined());
304+
};
305+
306+
// 3. Let errorData be the value of the [[ErrorData]] internal slot of E.
307+
// 4. If errorData is undefined, return undefined.
308+
let Some(error_data) = e.downcast_ref::<Error>() else {
309+
return Ok(JsValue::undefined());
310+
};
311+
312+
// 5. Let stackString be an implementation-defined String value representing the call stack.
313+
// 6. Return stackString.
314+
if let Some(backtrace) = error_data.location.as_ref().backtrace() {
315+
let stack_string = backtrace
316+
.iter()
317+
.rev()
318+
.map(|entry| format!(" at {}\n", entry.display(true)))
319+
.collect::<String>();
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+
let number_of_args = args.len();
345+
346+
// 4. If numberOfArgs is 0, throw a TypeError exception.
347+
if number_of_args == 0 {
348+
return Err(JsNativeError::typ()
349+
.with_message(
350+
"Error.prototype.stack setter requires at least 1 argument, but only 0 were passed",
351+
)
352+
.into());
353+
}
354+
355+
// 5. Return ? CreateDataPropertyOrThrow(E, "stack", value).
356+
e.create_data_property_or_throw(js_string!("stack"), args[0].clone(), context)
357+
.map(Into::into)
358+
}
359+
266360
/// `Error.prototype.toString()`
267361
///
268362
/// The `toString()` method returns a string representing the specified Error object.

0 commit comments

Comments
 (0)