Skip to main content

script/dom/
console.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::convert::TryFrom;
6use std::fmt::Write as _;
7use std::ptr::{self, NonNull};
8use std::slice;
9
10#[cfg(feature = "devtools")]
11use devtools_traits::{
12    ConsoleMessage, ConsoleMessageFields, DebuggerValue, FunctionPreview, ObjectPreview,
13    PropertyDescriptor as DevtoolsPropertyDescriptor, ScriptToDevtoolsControlMsg, StackFrame,
14    get_time_stamp,
15};
16use embedder_traits::{ConsoleLogLevel, EmbedderMsg};
17use js::context::JSContext;
18use js::conversions::jsstr_to_string;
19use js::jsapi::{self, ESClass, PropertyDescriptor};
20#[cfg(feature = "devtools")]
21use js::jsapi::{JS_GetFunctionArity, SavedFrameSelfHosted};
22use js::jsval::{Int32Value, ObjectValue, UndefinedValue};
23#[cfg(feature = "devtools")]
24use js::realm::CurrentRealm;
25#[cfg(feature = "devtools")]
26use js::rust::wrappers2::{
27    GetArrayLength, GetSavedFrameColumn, GetSavedFrameFunctionDisplayName, GetSavedFrameLine,
28    GetSavedFrameSource, JS_GetElement, JS_GetFunctionDisplayId, JS_GetFunctionId,
29    JS_ValueToFunction, MapEntries, MapSize,
30};
31use js::rust::wrappers2::{
32    GetBuiltinClass, JS_ClearPendingException, JS_Stringify, JS_ValueToSource,
33};
34#[cfg(feature = "devtools")]
35use js::rust::{CapturedJSStack, describe_scripted_caller, for_of};
36use js::rust::{HandleObject, HandleValue, IdVector, ToNumber, ToString};
37use script_bindings::conversions::get_dom_class;
38use script_bindings::js::{
39    GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById, JS_GetPrototype,
40    JS_IdToValue,
41};
42
43use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
44#[cfg(feature = "devtools")]
45use crate::dom::bindings::error::report_pending_exception;
46#[cfg(feature = "devtools")]
47use crate::dom::bindings::inheritance::Castable;
48use crate::dom::bindings::str::DOMString;
49use crate::dom::globalscope::GlobalScope;
50#[cfg(feature = "devtools")]
51use crate::dom::workerglobalscope::WorkerGlobalScope;
52
53/// The maximum object depth logged by console methods.
54const MAX_LOG_DEPTH: usize = 10;
55/// The maximum elements in an object logged by console methods.
56const MAX_LOG_CHILDREN: usize = 15;
57
58/// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
59#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
60pub(crate) struct Console;
61
62impl Console {
63    /// Helper to send a message that only consists of a single string
64    fn send_string_message(
65        cx: &mut JSContext,
66        global: &GlobalScope,
67        level: ConsoleLogLevel,
68        message: String,
69    ) {
70        let prefix = global.current_group_label().unwrap_or_default();
71        let formatted_message = format!("{prefix}{message}");
72
73        Self::send_to_embedder(global, level.clone(), formatted_message);
74
75        #[cfg(feature = "devtools")]
76        {
77            let console_message =
78                Self::build_message(cx, level, vec![DebuggerValue::StringValue(message)], None);
79            Self::send_to_devtools(global, console_message);
80        }
81
82        let _ = cx;
83    }
84
85    fn method(
86        cx: &mut JSContext,
87        global: &GlobalScope,
88        level: ConsoleLogLevel,
89        messages: Vec<HandleValue>,
90        include_stacktrace: IncludeStackTrace,
91    ) {
92        #[cfg(feature = "devtools")]
93        let devtools_arguments: Vec<DebuggerValue>;
94
95        // If the first argument is a string, apply sprintf-style substitutions per the
96        // WHATWG Console spec formatter. The result is a single formatted string followed
97        // by any arguments that were not consumed by a substitution specifier.
98        let embedder_msg = if !messages.is_empty() && messages[0].is_string() {
99            let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
100            let remaining = &messages[consumed..];
101
102            #[cfg(feature = "devtools")]
103            {
104                let mut arguments: Vec<DebuggerValue> =
105                    vec![DebuggerValue::StringValue(formatted.clone())];
106                for msg in remaining {
107                    arguments.push(devtools::console_argument_from_handle_value(
108                        cx,
109                        *msg,
110                        &mut Vec::new(),
111                    ));
112                }
113                devtools_arguments = arguments;
114            }
115
116            let embedder_msg = if remaining.is_empty() {
117                formatted
118            } else {
119                format!("{formatted} {}", stringify_handle_values(cx, remaining))
120            };
121
122            embedder_msg.into()
123        } else {
124            #[cfg(feature = "devtools")]
125            {
126                let arguments: Vec<DebuggerValue> = messages
127                    .iter()
128                    .map(|msg| {
129                        devtools::console_argument_from_handle_value(cx, *msg, &mut Vec::new())
130                    })
131                    .collect();
132                devtools_arguments = arguments;
133            }
134
135            stringify_handle_values(cx, &messages)
136        };
137
138        #[cfg(feature = "devtools")]
139        {
140            let stacktrace =
141                (include_stacktrace == IncludeStackTrace::Yes).then_some(get_js_stack(cx));
142            Console::send_to_devtools(
143                global,
144                Self::build_message(cx, level.clone(), devtools_arguments, stacktrace),
145            );
146        }
147
148        let prefix = global.current_group_label().unwrap_or_default();
149        let formatted_message = format!("{prefix}{embedder_msg}");
150
151        Self::send_to_embedder(global, level, formatted_message);
152    }
153
154    fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
155        global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
156            global.webview_id(),
157            level,
158            message,
159        ));
160    }
161
162    // Directly logs a string message, without processing the message
163    pub(crate) fn internal_warn(cx: &mut JSContext, global: &GlobalScope, message: String) {
164        Console::send_string_message(cx, global, ConsoleLogLevel::Warn, message);
165    }
166
167    pub(crate) fn internal_error(cx: &mut JSContext, global: &GlobalScope, message: String) {
168        Console::send_string_message(cx, global, ConsoleLogLevel::Error, message);
169    }
170}
171
172#[expect(unsafe_code)]
173fn handle_value_to_string(cx: &mut JSContext, value: HandleValue) -> DOMString {
174    match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
175        Some(js_str) => unsafe { jsstr_to_string(cx, js_str) }.into(),
176        None => DOMString::from_static("<error converting value to string>"),
177    }
178}
179
180#[cfg(feature = "devtools")]
181mod devtools {
182    use super::*;
183
184    impl Console {
185        pub(super) fn build_message(
186            cx: &mut JSContext,
187            level: ConsoleLogLevel,
188            arguments: Vec<DebuggerValue>,
189            stacktrace: Option<Vec<StackFrame>>,
190        ) -> ConsoleMessage {
191            let caller = describe_scripted_caller(cx).unwrap_or_default();
192
193            ConsoleMessage {
194                fields: ConsoleMessageFields {
195                    level,
196                    filename: caller.filename,
197                    line_number: caller.line,
198                    column_number: caller.col,
199                    time_stamp: get_time_stamp(),
200                },
201                arguments,
202                stacktrace,
203            }
204        }
205
206        pub(super) fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
207            if let Some(chan) = global.devtools_chan() {
208                let worker_id = global
209                    .downcast::<WorkerGlobalScope>()
210                    .map(|worker| worker.worker_id());
211                let devtools_message = ScriptToDevtoolsControlMsg::ConsoleAPI(
212                    global.pipeline_id(),
213                    message,
214                    worker_id,
215                );
216                chan.send(devtools_message).unwrap();
217            }
218        }
219    }
220
221    pub(super) fn console_argument_from_handle_value(
222        cx: &mut JSContext,
223        handle_value: HandleValue,
224        seen: &mut Vec<u64>,
225    ) -> DebuggerValue {
226        #[expect(unsafe_code)]
227        fn inner(
228            cx: &mut JSContext,
229            handle_value: HandleValue,
230            seen: &mut Vec<u64>,
231        ) -> Result<DebuggerValue, ()> {
232            if handle_value.is_string() {
233                let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
234                let dom_string = unsafe { jsstr_to_string(cx, js_string) };
235                return Ok(DebuggerValue::StringValue(dom_string));
236            }
237
238            if handle_value.is_number() {
239                let number = handle_value.to_number();
240                return Ok(DebuggerValue::NumberValue(number));
241            }
242
243            if handle_value.is_boolean() {
244                let boolean = handle_value.to_boolean();
245                return Ok(DebuggerValue::BooleanValue(boolean));
246            }
247
248            if handle_value.is_object() {
249                // JS objects can create circular reference, and we want to avoid recursing infinitely
250                if seen.contains(&handle_value.asBits_) {
251                    // FIXME: Handle this properly
252                    return Ok(DebuggerValue::StringValue("[circular]".into()));
253                }
254
255                seen.push(handle_value.asBits_);
256                rooted!(&in(cx) let object = handle_value.to_object());
257                let console_object = console_object_from_handle_value(cx, handle_value, seen);
258                let prototype = object_prototype_debugger_value(cx, object.handle(), seen)?;
259                let js_value = seen.pop();
260                debug_assert_eq!(js_value, Some(handle_value.asBits_));
261
262                if let Some((class, preview)) = console_object {
263                    return Ok(DebuggerValue::ObjectValue {
264                        actor: None,
265                        class,
266                        own_property_length: preview.own_properties_length,
267                        preview: Some(Box::new(preview)),
268                        prototype,
269                    });
270                }
271
272                return Err(());
273            }
274
275            // FIXME: Handle more complex argument types here
276            let stringified_value = stringify_handle_value(cx, handle_value);
277
278            Ok(DebuggerValue::StringValue(stringified_value.into()))
279        }
280
281        match inner(cx, handle_value, seen) {
282            Ok(arg) => arg,
283            Err(()) => {
284                report_pending_exception(&mut CurrentRealm::assert(cx));
285                DebuggerValue::StringValue("<error>".into())
286            },
287        }
288    }
289
290    #[cfg(feature = "devtools")]
291    fn object_prototype_debugger_value(
292        cx: &mut JSContext,
293        object: HandleObject,
294        seen: &mut Vec<u64>,
295    ) -> Result<Option<Box<DebuggerValue>>, ()> {
296        rooted!(&in(cx) let mut prototype = ptr::null_mut::<jsapi::JSObject>());
297        if !JS_GetPrototype(cx, object, prototype.handle_mut()) {
298            return Err(());
299        }
300        if prototype.is_null() {
301            return Ok(Some(Box::new(DebuggerValue::NullValue(false))));
302        }
303
304        rooted!(&in(cx) let prototype = ObjectValue(prototype.get()));
305        Ok(Some(Box::new(console_argument_from_handle_value(
306            cx,
307            prototype.handle(),
308            seen,
309        ))))
310    }
311
312    fn accessor_value_from_property_descriptor(descriptor: &PropertyDescriptor) -> DebuggerValue {
313        // https://console.spec.whatwg.org/#printer
314        // Objects with either generic JavaScript object formatting or optimally useful formatting applied.
315        let value = match (
316            descriptor.hasGetter_() && !descriptor.getter_.is_null(),
317            descriptor.hasSetter_() && !descriptor.setter_.is_null(),
318        ) {
319            (true, true) => "Getter/Setter",
320            (true, false) => "Getter",
321            (false, true) => "Setter",
322            (false, false) => "undefined",
323        };
324        DebuggerValue::StringValue(value.into())
325    }
326
327    #[expect(unsafe_code)]
328    fn console_map_object_from_handle_value(
329        cx: &mut JSContext,
330        handle_object: HandleObject,
331        seen: &mut Vec<u64>,
332    ) -> Option<(String, ObjectPreview)> {
333        rooted!(&in(cx) let mut iterator = UndefinedValue());
334        if !unsafe { MapEntries(cx, handle_object, iterator.handle_mut()) } {
335            return None;
336        }
337
338        let mut entries = Vec::new();
339        for_of(cx, iterator.handle(), |cx, entry| {
340            if !entry.is_object() {
341                return Err(().into());
342            }
343
344            rooted!(&in(cx) let entry_object = entry.to_object());
345            rooted!(&in(cx) let mut key = UndefinedValue());
346            rooted!(&in(cx) let mut value = UndefinedValue());
347
348            // Each map entry is a [key, value] pair.
349            if !unsafe { JS_GetElement(cx, entry_object.handle(), 0, key.handle_mut()) } ||
350                !unsafe { JS_GetElement(cx, entry_object.handle(), 1, value.handle_mut()) }
351            {
352                return Err(().into());
353            }
354
355            entries.push((
356                console_argument_from_handle_value(cx, key.handle(), seen),
357                console_argument_from_handle_value(cx, value.handle(), seen),
358            ));
359
360            Ok(std::ops::ControlFlow::Continue(()))
361        })
362        .ok()?;
363
364        Some((
365            "Map".into(),
366            ObjectPreview {
367                kind: "MapLike".into(),
368                size: Some(unsafe { MapSize(cx, handle_object) }),
369                entries: Some(entries),
370                own_properties_length: Some(0),
371                own_properties: None,
372                function: None,
373                array_length: None,
374                items: None,
375            },
376        ))
377    }
378
379    #[expect(unsafe_code)]
380    fn console_object_from_handle_value(
381        cx: &mut JSContext,
382        handle_value: HandleValue,
383        seen: &mut Vec<u64>,
384    ) -> Option<(String, ObjectPreview)> {
385        rooted!(&in(cx) let object = handle_value.to_object());
386        let mut object_class = ESClass::Other;
387        if !unsafe { GetBuiltinClass(cx, object.handle(), &mut object_class as *mut _) } {
388            return None;
389        }
390        if object_class != ESClass::Object &&
391            object_class != ESClass::Array &&
392            object_class != ESClass::Map &&
393            object_class != ESClass::Function
394        {
395            return None;
396        }
397
398        if object_class == ESClass::Map {
399            return console_map_object_from_handle_value(cx, object.handle(), seen);
400        }
401
402        let mut own_properties = Vec::new();
403        let mut items: Vec<(i32, DebuggerValue)> = Vec::new();
404        let mut ids = IdVector::new(cx);
405        // https://console.spec.whatwg.org/#printer
406        // Objects with either generic JavaScript object formatting or optimally useful formatting applied.
407        if !GetPropertyKeys(
408            cx,
409            object.handle(),
410            jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
411            ids.handle_mut(),
412        ) {
413            return None;
414        }
415
416        for id in ids.iter() {
417            rooted!(&in(cx) let id = *id);
418            rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
419
420            let mut is_none = false;
421            if !JS_GetOwnPropertyDescriptorById(
422                cx,
423                object.handle(),
424                id.handle(),
425                descriptor.handle_mut(),
426                &mut is_none,
427            ) {
428                return None;
429            }
430            if is_none {
431                continue;
432            }
433
434            // https://console.spec.whatwg.org/#printer
435            // Objects with either generic JavaScript object formatting or optimally useful formatting applied.
436            let is_accessor = (descriptor.hasGetter_() && !descriptor.getter_.is_null()) ||
437                (descriptor.hasSetter_() && !descriptor.setter_.is_null());
438            let value = if is_accessor {
439                accessor_value_from_property_descriptor(&descriptor)
440            } else {
441                rooted!(&in(cx) let property = descriptor.value_);
442                console_argument_from_handle_value(cx, property.handle(), seen)
443            };
444
445            if object_class == ESClass::Array && id.is_int() {
446                let index = id.to_int();
447                items.push((index, value));
448                continue;
449            }
450
451            let key = if id.is_string() {
452                rooted!(&in(cx) let mut key_value = UndefinedValue());
453                if !JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) {
454                    continue;
455                }
456                rooted!(&in(cx) let js_string = key_value.to_string());
457                let Some(js_string) = NonNull::new(js_string.get()) else {
458                    continue;
459                };
460                unsafe { jsstr_to_string(cx, js_string) }
461            } else if id.is_symbol() || id.is_int() {
462                rooted!(&in(cx) let mut key_value = UndefinedValue());
463                if !JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) {
464                    continue;
465                }
466                handle_value_to_string(cx, key_value.handle()).to_string()
467            } else {
468                continue;
469            };
470
471            own_properties.push(DevtoolsPropertyDescriptor {
472                name: key,
473                value,
474                configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
475                enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
476                writable: !is_accessor && descriptor.hasWritable_() && descriptor.writable_(),
477                is_accessor,
478            });
479        }
480
481        let (class, kind, function, array_length, items) = match object_class {
482            ESClass::Array => {
483                let mut len = 0u32;
484                if !unsafe { GetArrayLength(cx, object.handle(), &mut len) } {
485                    return None;
486                }
487                items.sort_by_key(|(index, _)| *index);
488                let ordered: Vec<DebuggerValue> =
489                    items.into_iter().map(|(_, value)| value).collect();
490                (
491                    "Array".into(),
492                    "ArrayLike".into(),
493                    None,
494                    Some(len),
495                    Some(ordered),
496                )
497            },
498            ESClass::Function => {
499                rooted!(&in(cx) let fun = unsafe { JS_ValueToFunction(cx, handle_value) });
500                rooted!(&in(cx) let mut name = std::ptr::null_mut::<jsapi::JSString>());
501                rooted!(&in(cx) let mut display_name = std::ptr::null_mut::<jsapi::JSString>());
502                let arity;
503                unsafe {
504                    JS_GetFunctionId(cx, fun.handle(), name.handle_mut());
505                    JS_GetFunctionDisplayId(cx, fun.handle(), display_name.handle_mut());
506                    arity = JS_GetFunctionArity(fun.get());
507                }
508                let name =
509                    ptr::NonNull::new(*name).map(|name| unsafe { jsstr_to_string(cx, name) });
510                let display_name = ptr::NonNull::new(*display_name)
511                    .map(|display_name| unsafe { jsstr_to_string(cx, display_name) });
512
513                // TODO: We should get the actual argument names from the function
514                // It's not trivial since we can't access the debugger API here
515                let parameter_names = (0..arity).map(|i| format!("<arg{i}>")).collect();
516
517                let function = Box::new(FunctionPreview {
518                    name,
519                    display_name,
520                    parameter_names,
521                    is_async: None,
522                    is_generator: None,
523                });
524                (
525                    "Function".into(),
526                    "Object".into(),
527                    Some(function),
528                    None,
529                    None,
530                )
531            },
532            // TODO: Investigate if this class should be the object class
533            _ => ("Object".into(), "Object".into(), None, None, None),
534        };
535
536        Some((
537            class,
538            ObjectPreview {
539                kind,
540                size: None,
541                entries: None,
542                own_properties_length: Some(own_properties.len() as u32),
543                own_properties: Some(own_properties),
544                function,
545                array_length,
546                items,
547            },
548        ))
549    }
550
551    pub(super) fn string_array_debugger_value(strings: &[DOMString]) -> DebuggerValue {
552        let items: Vec<DebuggerValue> = strings
553            .iter()
554            .map(|s| DebuggerValue::StringValue(s.to_string()))
555            .collect();
556        DebuggerValue::ObjectValue {
557            actor: None,
558            class: "Array".into(),
559            own_property_length: Some(0),
560            preview: Some(Box::new(ObjectPreview {
561                kind: "ArrayLike".into(),
562                size: None,
563                entries: None,
564                own_properties_length: Some(0),
565                own_properties: None,
566                function: None,
567                array_length: Some(items.len() as u32),
568                items: Some(items),
569            })),
570            prototype: None,
571        }
572    }
573}
574
575#[expect(unsafe_code)]
576pub(crate) fn stringify_handle_value(cx: &mut JSContext, message: HandleValue) -> DOMString {
577    if message.is_string() {
578        let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
579        return unsafe { jsstr_to_string(cx, jsstr) }.into();
580    }
581    fn stringify_object_from_handle_value(
582        cx: &mut JSContext,
583        value: HandleValue,
584        parents: Vec<u64>,
585    ) -> DOMString {
586        rooted!(&in(cx) let mut obj = value.to_object());
587        let mut object_class = ESClass::Other;
588        if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
589            return DOMString::from_static("/* invalid */");
590        }
591        let mut ids = IdVector::new(cx);
592        if !GetPropertyKeys(
593            cx,
594            obj.handle(),
595            jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
596            ids.handle_mut(),
597        ) {
598            return DOMString::from_static("/* invalid */");
599        }
600        let truncate = ids.len() > MAX_LOG_CHILDREN;
601        if object_class != ESClass::Array && object_class != ESClass::Object {
602            if truncate {
603                return DOMString::from_static("…");
604            } else {
605                return handle_value_to_string(cx, value);
606            }
607        }
608
609        let mut explicit_keys = object_class == ESClass::Object;
610        let mut props = Vec::with_capacity(ids.len());
611        for id in ids.iter().take(MAX_LOG_CHILDREN) {
612            rooted!(&in(cx) let id = *id);
613            rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
614
615            let mut is_none = false;
616            if !JS_GetOwnPropertyDescriptorById(
617                cx,
618                obj.handle(),
619                id.handle(),
620                desc.handle_mut(),
621                &mut is_none,
622            ) {
623                return DOMString::from_static("/* invalid */");
624            }
625
626            rooted!(&in(cx) let mut property = UndefinedValue());
627            if !JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) {
628                return DOMString::from_static("/* invalid */");
629            }
630
631            if !explicit_keys {
632                if id.is_int() {
633                    if let Ok(id_int) = usize::try_from(id.to_int()) {
634                        explicit_keys = props.len() != id_int;
635                    } else {
636                        explicit_keys = false;
637                    }
638                } else {
639                    explicit_keys = false;
640                }
641            }
642            let value_string = stringify_inner(cx, property.handle(), parents.clone());
643            if explicit_keys {
644                let key = if id.is_string() || id.is_symbol() || id.is_int() {
645                    rooted!(&in(cx) let mut key_value = UndefinedValue());
646                    if !JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) {
647                        return DOMString::from_static("/* invalid */");
648                    }
649                    handle_value_to_string(cx, key_value.handle())
650                } else {
651                    return DOMString::from_static("/* invalid */");
652                };
653                props.push(format!("{}: {}", key, value_string,));
654            } else {
655                props.push(String::from(value_string));
656            }
657        }
658        if truncate {
659            props.push("…".to_string());
660        }
661        if object_class == ESClass::Array {
662            DOMString::from(format!("[{}]", itertools::join(props, ", ")))
663        } else {
664            DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
665        }
666    }
667    fn stringify_inner(cx: &mut JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
668        if parents.len() >= MAX_LOG_DEPTH {
669            return DOMString::from_static("...");
670        }
671        let value_bits = value.asBits_;
672        if parents.contains(&value_bits) {
673            return DOMString::from_static("[circular]");
674        }
675        if value.is_undefined() {
676            // This produces a better value than "(void 0)" from JS_ValueToSource.
677            return DOMString::from_static("undefined");
678        } else if !value.is_object() {
679            return handle_value_to_string(cx, value);
680        }
681        parents.push(value_bits);
682
683        if value.is_object() &&
684            let Some(repr) = maybe_stringify_dom_object(cx, value)
685        {
686            return repr;
687        }
688        stringify_object_from_handle_value(cx, value, parents)
689    }
690    stringify_inner(cx, message, Vec::new())
691}
692
693#[expect(unsafe_code)]
694fn maybe_stringify_dom_object(cx: &mut JSContext, value: HandleValue) -> Option<DOMString> {
695    // The standard object serialization is not effective for DOM objects,
696    // since their properties generally live on the prototype object.
697    // Instead, fall back to the output of JSON.stringify combined
698    // with the class name extracted from the output of toString().
699    rooted!(&in(cx) let obj = value.to_object());
700    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
701    if !is_dom_class {
702        return None;
703    }
704    rooted!(&in(cx) let class_name = unsafe { ToString(cx, value) });
705    let Some(class_name) = NonNull::new(class_name.get()) else {
706        return Some(DOMString::from_static(
707            "<error converting DOM object to string>",
708        ));
709    };
710    let class_name = unsafe { jsstr_to_string(cx, class_name) }
711        .replace("[object ", "")
712        .replace("]", "");
713    let mut repr = format!("{} ", class_name);
714    rooted!(&in(cx) let mut value = value.get());
715
716    #[expect(unsafe_code)]
717    unsafe extern "C" fn stringified(
718        string: *const u16,
719        len: u32,
720        data: *mut std::ffi::c_void,
721    ) -> bool {
722        let s = data as *mut String;
723        let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
724        unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
725        true
726    }
727
728    rooted!(&in(cx) let space = Int32Value(2));
729    let stringify_result = unsafe {
730        JS_Stringify(
731            cx,
732            value.handle_mut(),
733            HandleObject::null(),
734            space.handle(),
735            Some(stringified),
736            &mut repr as *mut String as *mut _,
737        )
738    };
739    if !stringify_result {
740        return Some(DOMString::from_static(
741            "<error converting DOM object to string>",
742        ));
743    }
744    Some(repr.into())
745}
746
747/// Apply sprintf-style substitutions to console format arguments per the WHATWG Console spec.
748///
749/// If the first argument is a string, it is treated as a format string where `%s`, `%d`, `%i`,
750/// `%f`, `%o`, `%O`, and `%c` are replaced by subsequent arguments. Returns the formatted string
751/// and the index of the first argument that was not consumed by a substitution.
752///
753/// <https://console.spec.whatwg.org/#formatter>
754#[expect(unsafe_code)]
755fn apply_sprintf_substitutions(cx: &mut JSContext, messages: &[HandleValue]) -> (String, usize) {
756    debug_assert!(!messages.is_empty() && messages[0].is_string());
757
758    let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
759    let format_string = unsafe { jsstr_to_string(cx, js_string) };
760
761    let mut result = String::new();
762    let mut arg_index = 1usize;
763    let mut chars = format_string.chars().peekable();
764
765    while let Some(c) = chars.next() {
766        if c != '%' {
767            result.push(c);
768            continue;
769        }
770
771        match chars.peek().copied() {
772            Some('s') => {
773                chars.next();
774                if arg_index < messages.len() {
775                    result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
776                    arg_index += 1;
777                } else {
778                    result.push_str("%s");
779                }
780            },
781            Some('d') | Some('i') => {
782                let spec = chars.next().unwrap();
783                if arg_index < messages.len() {
784                    let num = unsafe { ToNumber(cx, messages[arg_index]) };
785                    if num.is_err() {
786                        unsafe { JS_ClearPendingException(cx) };
787                    }
788                    arg_index += 1;
789                    format_integer_substitution(&mut result, num);
790                } else {
791                    result.push('%');
792                    result.push(spec);
793                }
794            },
795            Some('f') => {
796                chars.next();
797                if arg_index < messages.len() {
798                    let num = unsafe { ToNumber(cx, messages[arg_index]) };
799                    if num.is_err() {
800                        unsafe { JS_ClearPendingException(cx) };
801                    }
802                    arg_index += 1;
803                    format_float_substitution(&mut result, num);
804                } else {
805                    result.push_str("%f");
806                }
807            },
808            Some('o') | Some('O') => {
809                let spec = chars.next().unwrap();
810                if arg_index < messages.len() {
811                    result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
812                    arg_index += 1;
813                } else {
814                    result.push('%');
815                    result.push(spec);
816                }
817            },
818            Some('c') => {
819                chars.next();
820                if arg_index < messages.len() {
821                    arg_index += 1; // consume but ignore CSS styling
822                }
823            },
824            Some('%') => {
825                chars.next();
826                result.push('%');
827            },
828            _ => {
829                result.push('%');
830            },
831        }
832    }
833
834    (result, arg_index)
835}
836
837fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
838    match num {
839        Ok(n) if n.is_nan() => result.push_str("NaN"),
840        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
841        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
842        Ok(n) => write!(result, "{}", n.trunc() as i64).unwrap(),
843        Err(_) => result.push_str("NaN"),
844    }
845}
846
847fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
848    match num {
849        Ok(n) if n.is_nan() => result.push_str("NaN"),
850        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
851        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
852        Ok(n) => write!(result, "{n}").unwrap(),
853        Err(_) => result.push_str("NaN"),
854    }
855}
856
857fn stringify_handle_values(cx: &mut JSContext, messages: &[HandleValue]) -> DOMString {
858    DOMString::from(itertools::join(
859        messages
860            .iter()
861            .copied()
862            .map(|msg| stringify_handle_value(cx, msg)),
863        " ",
864    ))
865}
866
867/// An implementation of <https://console.spec.whatwg.org/#printer>.
868/// This produces a string version of the argument that is printed to the console.
869#[cfg(feature = "devtools")]
870fn stringify_debugger_value(value: &DebuggerValue) -> String {
871    match value {
872        DebuggerValue::VoidValue => "undefined".into(),
873        DebuggerValue::NullValue(_) => "null".into(),
874        DebuggerValue::BooleanValue(value) => value.to_string(),
875        DebuggerValue::NumberValue(value) => value.to_string(),
876        DebuggerValue::StringValue(value) => value.clone(),
877        DebuggerValue::ObjectValue { class, preview, .. } => {
878            let Some(preview) = preview else {
879                return class.clone();
880            };
881
882            if preview.kind == "ArrayLike" {
883                let mut items = preview
884                    .items
885                    .as_ref()
886                    .map(|items| {
887                        items
888                            .iter()
889                            .take(MAX_LOG_CHILDREN)
890                            .map(stringify_debugger_value)
891                            .collect::<Vec<_>>()
892                    })
893                    .unwrap_or_default();
894                if preview
895                    .array_length
896                    .is_some_and(|length| length as usize > items.len())
897                {
898                    items.push("...".into());
899                }
900                return format!("[{}]", itertools::join(items, ", "));
901            }
902
903            let mut properties = preview
904                .own_properties
905                .as_ref()
906                .map(|properties| {
907                    properties
908                        .iter()
909                        .take(MAX_LOG_CHILDREN)
910                        .map(|property| {
911                            format!(
912                                "{}: {}",
913                                property.name,
914                                stringify_debugger_value(&property.value)
915                            )
916                        })
917                        .collect::<Vec<_>>()
918                })
919                .unwrap_or_default();
920            if preview
921                .own_properties_length
922                .is_some_and(|length| length as usize > properties.len())
923            {
924                properties.push("...".into());
925            }
926            format!("{class} {{{}}}", itertools::join(properties, ", "))
927        },
928    }
929}
930
931#[derive(Default)]
932struct ConsoleTable {
933    /// Column names in first-seen order (or the order given by `properties`).
934    columns: Vec<String>,
935    rows: Vec<TableRow>,
936}
937
938struct TableRow {
939    index: String,
940    /// (column name, cell text)
941    cells: Vec<(String, String)>,
942    /// Set when the row itself is a primitive -> goes in the "Values" column.
943    value: Option<String>,
944}
945
946impl ConsoleTable {
947    fn render(&self) -> String {
948        let has_values = self.rows.iter().any(|row| row.value.is_some());
949
950        let mut headers = vec!["(index)".to_string()];
951        headers.extend(self.columns.iter().cloned());
952        if has_values {
953            headers.push("Values".into());
954        }
955
956        let grid: Vec<Vec<String>> = self
957            .rows
958            .iter()
959            .map(|row| {
960                let mut line = vec![row.index.clone()];
961                for column in &self.columns {
962                    let cell = row
963                        .cells
964                        .iter()
965                        .find(|(key, _)| key == column)
966                        .map(|(_, v)| v.clone())
967                        .unwrap_or_default();
968                    line.push(cell);
969                }
970                if has_values {
971                    line.push(row.value.clone().unwrap_or_default());
972                }
973                line
974            })
975            .collect();
976
977        let widths: Vec<usize> = (0..headers.len())
978            .map(|i| {
979                std::iter::once(&headers[i])
980                    .chain(grid.iter().map(|row| &row[i]))
981                    .map(|s| s.chars().count())
982                    .max()
983                    .unwrap_or(0)
984            })
985            .collect();
986
987        let border = |left: &str, mid: &str, right: &str| {
988            let segments: Vec<String> = widths.iter().map(|w| "─".repeat(w + 2)).collect();
989            format!("{left}{}{right}", segments.join(mid))
990        };
991        let line = |cells: &[String]| {
992            let padded: Vec<String> = cells
993                .iter()
994                .zip(&widths)
995                .map(|(cell, &w)| format!(" {cell:<w$} "))
996                .collect();
997            format!("│{}│", padded.join("│"))
998        };
999
1000        let mut out = vec![border("┌", "┬", "┐"), line(&headers), border("├", "┼", "┤")];
1001        out.extend(grid.iter().map(|row| line(row)));
1002        out.push(border("└", "┴", "┘"));
1003        out.join("\n")
1004    }
1005}
1006
1007/// Calls `f(cx, key, value)` for each own enumerable string/index property of `obj`.
1008/// Returns false if a JSAPI call failed (an exception may be pending).
1009fn for_each_own_property(
1010    cx: &mut JSContext,
1011    obj: HandleObject,
1012    mut f: impl FnMut(&mut JSContext, String, HandleValue),
1013) -> bool {
1014    let mut ids = IdVector::new(cx);
1015    // No JSITER_HIDDEN -> enumerable only; no JSITER_SYMBOLS -> skip symbols (like Chrome/Firefox).
1016    if !GetPropertyKeys(cx, obj, jsapi::JSITER_OWNONLY, ids.handle_mut()) {
1017        return false;
1018    }
1019    for id in ids.iter() {
1020        rooted!(&in(cx) let id = *id);
1021        rooted!(&in(cx) let mut key = UndefinedValue());
1022        if !JS_IdToValue(cx, id.handle().get(), key.handle_mut()) {
1023            return false;
1024        }
1025        let key = stringify_handle_value(cx, key.handle()).to_string();
1026
1027        rooted!(&in(cx) let mut value = UndefinedValue());
1028        // Note: this can run getters, which can throw.
1029        if !JS_GetPropertyById(cx, obj, id.handle(), value.handle_mut()) {
1030            return false;
1031        }
1032        f(cx, key, value.handle());
1033    }
1034    true
1035}
1036
1037fn table_cell_string(cx: &mut JSContext, value: HandleValue) -> String {
1038    if value.is_string() {
1039        handle_value_to_string(cx, value).to_string() // JS_ValueToSource -> "\"Anna\""
1040    } else {
1041        stringify_handle_value(cx, value).to_string()
1042    }
1043}
1044
1045#[expect(unsafe_code)]
1046fn build_console_table(
1047    cx: &mut JSContext,
1048    data: HandleValue,
1049    properties: Option<&[DOMString]>,
1050) -> Option<ConsoleTable> {
1051    if !data.is_object() {
1052        return None;
1053    }
1054    let filter: Option<Vec<String>> =
1055        properties.map(|props| props.iter().map(|p| p.to_string()).collect());
1056
1057    let mut table = ConsoleTable::default();
1058    if let Some(filter) = &filter {
1059        // With an explicit `properties` list, columns follow that order.
1060        table.columns = filter.clone();
1061    }
1062
1063    rooted!(&in(cx) let data_obj = data.to_object());
1064    let mut failed = false;
1065    let ok = for_each_own_property(cx, data_obj.handle(), |cx, index, row| {
1066        let mut cells = Vec::new();
1067        let mut value = None;
1068
1069        if row.is_object() {
1070            rooted!(&in(cx) let row_obj = row.to_object());
1071            let columns = &mut table.columns;
1072            let ok = for_each_own_property(cx, row_obj.handle(), |cx, key, cell| {
1073                match &filter {
1074                    Some(filter) if !filter.contains(&key) => return,
1075                    None if !columns.contains(&key) => columns.push(key.clone()),
1076                    _ => {},
1077                }
1078                cells.push((key, table_cell_string(cx, cell)));
1079            });
1080            failed |= !ok;
1081        } else {
1082            value = Some(table_cell_string(cx, row));
1083        }
1084
1085        table.rows.push(TableRow {
1086            index,
1087            cells,
1088            value,
1089        });
1090    });
1091
1092    if !ok || failed {
1093        unsafe { JS_ClearPendingException(cx) };
1094        return None;
1095    }
1096    Some(table)
1097}
1098
1099#[derive(Debug, Eq, PartialEq)]
1100enum IncludeStackTrace {
1101    Yes,
1102    No,
1103}
1104
1105impl consoleMethods<crate::DomTypeHolder> for Console {
1106    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/log>
1107    fn Log(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1108        Console::method(
1109            cx,
1110            global,
1111            ConsoleLogLevel::Log,
1112            messages,
1113            IncludeStackTrace::No,
1114        );
1115    }
1116
1117    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/table>
1118    fn Table(
1119        cx: &mut JSContext,
1120        global: &GlobalScope,
1121        tabular_data: HandleValue,
1122        properties: Option<Vec<DOMString>>,
1123    ) {
1124        let Some(table) = build_console_table(cx, tabular_data, properties.as_deref()) else {
1125            // Spec: "Fall back to just logging the argument if it can't be parsed as tabular."
1126            Console::method(
1127                cx,
1128                global,
1129                ConsoleLogLevel::Log,
1130                vec![tabular_data],
1131                IncludeStackTrace::No,
1132            );
1133            return;
1134        };
1135
1136        #[cfg(feature = "devtools")]
1137        {
1138            let data =
1139                devtools::console_argument_from_handle_value(cx, tabular_data, &mut Vec::new());
1140
1141            // Types we can't preview (e.g. Set today) come back as a string like "<error>".
1142            // Firefox's frontend also falls back to a plain log for unsupported data.
1143            let level = if matches!(data, DebuggerValue::ObjectValue { .. }) {
1144                ConsoleLogLevel::Table
1145            } else {
1146                ConsoleLogLevel::Log
1147            };
1148
1149            let mut arguments = vec![data];
1150            if let Some(properties) = &properties {
1151                arguments.push(devtools::string_array_debugger_value(properties));
1152            }
1153
1154            Console::send_to_devtools(global, Self::build_message(cx, level, arguments, None));
1155        }
1156
1157        let prefix = global.current_group_label().unwrap_or_default();
1158        Self::send_to_embedder(
1159            global,
1160            ConsoleLogLevel::Table,
1161            format!("{prefix}\n{}", table.render()),
1162        );
1163    }
1164
1165    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/clear>
1166    fn Clear(global: &GlobalScope) {
1167        #[cfg(feature = "devtools")]
1168        if let Some(chan) = global.devtools_chan() {
1169            let worker_id = global
1170                .downcast::<WorkerGlobalScope>()
1171                .map(|worker| worker.worker_id());
1172            let devtools_message =
1173                ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
1174            if let Err(error) = chan.send(devtools_message) {
1175                log::warn!("Error sending clear message to devtools: {error:?}");
1176            }
1177        }
1178
1179        let _ = global;
1180    }
1181
1182    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
1183    fn Debug(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1184        Console::method(
1185            cx,
1186            global,
1187            ConsoleLogLevel::Debug,
1188            messages,
1189            IncludeStackTrace::No,
1190        );
1191    }
1192
1193    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/info>
1194    fn Info(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1195        Console::method(
1196            cx,
1197            global,
1198            ConsoleLogLevel::Info,
1199            messages,
1200            IncludeStackTrace::No,
1201        );
1202    }
1203
1204    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/warn>
1205    fn Warn(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1206        Console::method(
1207            cx,
1208            global,
1209            ConsoleLogLevel::Warn,
1210            messages,
1211            IncludeStackTrace::No,
1212        );
1213    }
1214
1215    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/error>
1216    fn Error(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1217        Console::method(
1218            cx,
1219            global,
1220            ConsoleLogLevel::Error,
1221            messages,
1222            IncludeStackTrace::No,
1223        );
1224    }
1225
1226    /// <https://console.spec.whatwg.org/#trace>
1227    fn Trace(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1228        Console::method(
1229            cx,
1230            global,
1231            ConsoleLogLevel::Trace,
1232            messages,
1233            IncludeStackTrace::Yes,
1234        );
1235    }
1236
1237    /// <https://console.spec.whatwg.org/#dir>
1238    fn Dir(
1239        cx: &mut js::context::JSContext,
1240        global: &GlobalScope,
1241        item: HandleValue,
1242        _options: Option<*mut jsapi::JSObject>,
1243    ) {
1244        #[cfg(feature = "devtools")]
1245        {
1246            // Step 1. Let object be item with generic JavaScript object formatting applied.
1247            let argument = devtools::console_argument_from_handle_value(cx, item, &mut Vec::new());
1248            let prefix = global.current_group_label().unwrap_or_default();
1249            // Step 2. Perform Printer("dir", « object », options).
1250            Console::send_to_devtools(
1251                global,
1252                Self::build_message(cx, ConsoleLogLevel::Dir, vec![argument.clone()], None),
1253            );
1254            Self::send_to_embedder(
1255                global,
1256                ConsoleLogLevel::Dir,
1257                format!("{prefix}{}", stringify_debugger_value(&argument)),
1258            );
1259        }
1260
1261        #[cfg(not(feature = "devtools"))]
1262        {
1263            let prefix = global.current_group_label().unwrap_or_default();
1264            Self::send_to_embedder(
1265                global,
1266                ConsoleLogLevel::Dir,
1267                format!("{prefix}{}", stringify_handle_value(cx, item)),
1268            );
1269        }
1270    }
1271
1272    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/assert>
1273    fn Assert(
1274        cx: &mut JSContext,
1275        global: &GlobalScope,
1276        condition: bool,
1277        messages: Vec<HandleValue>,
1278    ) {
1279        if !condition {
1280            let message = format!(
1281                "Assertion failed: {}",
1282                stringify_handle_values(cx, &messages)
1283            );
1284
1285            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1286        }
1287    }
1288
1289    /// <https://console.spec.whatwg.org/#time>
1290    fn Time(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1291        if let Ok(()) = global.time(label.clone()) {
1292            let message = format!("{label}: timer started");
1293            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1294        }
1295    }
1296
1297    /// <https://console.spec.whatwg.org/#timelog>
1298    fn TimeLog(cx: &mut JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
1299        if let Ok(delta) = global.time_log(&label) {
1300            let message = format!("{label}: {delta}ms {}", stringify_handle_values(cx, &data));
1301
1302            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1303        }
1304    }
1305
1306    /// <https://console.spec.whatwg.org/#timeend>
1307    fn TimeEnd(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1308        if let Ok(delta) = global.time_end(&label) {
1309            let message = format!("{label}: {delta}ms");
1310
1311            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1312        }
1313    }
1314
1315    /// <https://console.spec.whatwg.org/#group>
1316    fn Group(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1317        global.push_console_group(stringify_handle_values(cx, &messages));
1318    }
1319
1320    /// <https://console.spec.whatwg.org/#groupcollapsed>
1321    fn GroupCollapsed(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1322        global.push_console_group(stringify_handle_values(cx, &messages));
1323    }
1324
1325    /// <https://console.spec.whatwg.org/#groupend>
1326    fn GroupEnd(global: &GlobalScope) {
1327        global.pop_console_group();
1328    }
1329
1330    /// <https://console.spec.whatwg.org/#count>
1331    fn Count(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1332        let count = global.increment_console_count(&label);
1333        let message = format!("{label}: {count}");
1334
1335        Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1336    }
1337
1338    /// <https://console.spec.whatwg.org/#countreset>
1339    fn CountReset(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1340        if global.reset_console_count(&label).is_err() {
1341            Self::internal_warn(cx, global, format!("Counter “{label}” doesn’t exist."))
1342        }
1343    }
1344}
1345
1346#[cfg(feature = "devtools")]
1347#[expect(unsafe_code)]
1348fn get_js_stack(cx: &mut JSContext) -> Vec<StackFrame> {
1349    const MAX_FRAME_COUNT: u32 = 128;
1350
1351    let mut frames = vec![];
1352    rooted!(&in(cx) let mut handle =  ptr::null_mut());
1353    let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
1354    let Some(mut captured_js_stack) = captured_js_stack else {
1355        return frames;
1356    };
1357
1358    captured_js_stack.for_each_stack_frame(|cx, frame| {
1359        rooted!(&in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
1360
1361        // Get function name
1362        unsafe {
1363            GetSavedFrameFunctionDisplayName(
1364                cx,
1365                ptr::null_mut(),
1366                frame,
1367                result.handle_mut(),
1368                SavedFrameSelfHosted::Include,
1369            );
1370        }
1371        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1372            unsafe { jsstr_to_string(cx, nonnull_result) }
1373        } else {
1374            "<anonymous>".into()
1375        };
1376
1377        // Get source file name
1378        result.set(ptr::null_mut());
1379        unsafe {
1380            GetSavedFrameSource(
1381                cx,
1382                ptr::null_mut(),
1383                frame,
1384                result.handle_mut(),
1385                SavedFrameSelfHosted::Include,
1386            );
1387        }
1388        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1389            unsafe { jsstr_to_string(cx, nonnull_result) }
1390        } else {
1391            "<anonymous>".into()
1392        };
1393
1394        // get line/column number
1395        let mut line_number = 0;
1396        unsafe {
1397            GetSavedFrameLine(
1398                cx,
1399                ptr::null_mut(),
1400                frame,
1401                &mut line_number,
1402                SavedFrameSelfHosted::Include,
1403            );
1404        }
1405
1406        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
1407        unsafe {
1408            GetSavedFrameColumn(
1409                cx,
1410                ptr::null_mut(),
1411                frame,
1412                &mut column_number,
1413                SavedFrameSelfHosted::Include,
1414            );
1415        }
1416        let frame = StackFrame {
1417            filename,
1418            function_name,
1419            line_number,
1420            column_number: column_number.value_,
1421        };
1422
1423        frames.push(frame);
1424    });
1425
1426    frames
1427}