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::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10    ConsoleLogLevel, ConsoleMessage, ConsoleMessageFields, DebuggerValue, ObjectPreview,
11    PropertyDescriptor as DevtoolsPropertyDescriptor, ScriptToDevtoolsControlMsg, StackFrame,
12    get_time_stamp,
13};
14use embedder_traits::EmbedderMsg;
15use js::conversions::jsstr_to_string;
16use js::jsapi::{self, ESClass, PropertyDescriptor};
17use js::jsval::{Int32Value, UndefinedValue};
18use js::rust::wrappers::{
19    GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
20    JS_IdToValue, JS_Stringify, JS_ValueToSource,
21};
22use js::rust::{
23    CapturedJSStack, HandleObject, HandleValue, IdVector, ToNumber, ToString,
24    describe_scripted_caller,
25};
26use script_bindings::conversions::get_dom_class;
27
28use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
29use crate::dom::bindings::error::report_pending_exception;
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::str::DOMString;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::workerglobalscope::WorkerGlobalScope;
34use crate::realms::{AlreadyInRealm, InRealm};
35use crate::script_runtime::{CanGc, JSContext};
36
37/// The maximum object depth logged by console methods.
38const MAX_LOG_DEPTH: usize = 10;
39/// The maximum elements in an object logged by console methods.
40const MAX_LOG_CHILDREN: usize = 15;
41
42/// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
43#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
44pub(crate) struct Console;
45
46impl Console {
47    #[expect(unsafe_code)]
48    fn build_message(
49        level: ConsoleLogLevel,
50        arguments: Vec<DebuggerValue>,
51        stacktrace: Option<Vec<StackFrame>>,
52    ) -> ConsoleMessage {
53        let cx = GlobalScope::get_cx();
54        let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
55
56        ConsoleMessage {
57            fields: ConsoleMessageFields {
58                level,
59                filename: caller.filename,
60                line_number: caller.line,
61                column_number: caller.col,
62                time_stamp: get_time_stamp(),
63            },
64            arguments,
65            stacktrace,
66        }
67    }
68
69    /// Helper to send a message that only consists of a single string
70    fn send_string_message(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
71        let prefix = global.current_group_label().unwrap_or_default();
72        let formatted_message = format!("{prefix}{message}");
73
74        Self::send_to_embedder(global, level.clone(), formatted_message);
75
76        let console_message =
77            Self::build_message(level, vec![DebuggerValue::StringValue(message)], None);
78
79        Self::send_to_devtools(global, console_message);
80    }
81
82    fn method(
83        global: &GlobalScope,
84        level: ConsoleLogLevel,
85        messages: Vec<HandleValue>,
86        include_stacktrace: IncludeStackTrace,
87    ) {
88        let cx = GlobalScope::get_cx();
89
90        // If the first argument is a string, apply sprintf-style substitutions per the
91        // WHATWG Console spec formatter. The result is a single formatted string followed
92        // by any arguments that were not consumed by a substitution specifier.
93        let (arguments, embedder_msg) = if !messages.is_empty() && messages[0].is_string() {
94            let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
95            let remaining = &messages[consumed..];
96
97            let mut arguments: Vec<DebuggerValue> =
98                vec![DebuggerValue::StringValue(formatted.clone())];
99            for msg in remaining {
100                arguments.push(console_argument_from_handle_value(
101                    cx,
102                    *msg,
103                    &mut Vec::new(),
104                ));
105            }
106
107            let embedder_msg = if remaining.is_empty() {
108                formatted
109            } else {
110                format!("{formatted} {}", stringify_handle_values(remaining))
111            };
112
113            (arguments, embedder_msg.into())
114        } else {
115            let arguments = messages
116                .iter()
117                .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
118                .collect();
119            (arguments, stringify_handle_values(&messages))
120        };
121
122        let stacktrace = (include_stacktrace == IncludeStackTrace::Yes)
123            .then_some(get_js_stack(*GlobalScope::get_cx()));
124        let console_message = Self::build_message(level.clone(), arguments, stacktrace);
125
126        Console::send_to_devtools(global, console_message);
127
128        let prefix = global.current_group_label().unwrap_or_default();
129        let formatted_message = format!("{prefix}{embedder_msg}");
130
131        Self::send_to_embedder(global, level, formatted_message);
132    }
133
134    fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
135        if let Some(chan) = global.devtools_chan() {
136            let worker_id = global
137                .downcast::<WorkerGlobalScope>()
138                .map(|worker| worker.worker_id());
139            let devtools_message =
140                ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
141            chan.send(devtools_message).unwrap();
142        }
143    }
144
145    fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
146        global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
147            global.webview_id(),
148            level,
149            message,
150        ));
151    }
152
153    // Directly logs a string message, without processing the message
154    pub(crate) fn internal_warn(global: &GlobalScope, message: String) {
155        Console::send_string_message(global, ConsoleLogLevel::Warn, message);
156    }
157}
158
159#[expect(unsafe_code)]
160unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
161    rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
162    match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
163        Some(js_str) => {
164            js_string.set(js_str.as_ptr());
165            unsafe { jsstr_to_string(cx, js_str) }.into()
166        },
167        None => "<error converting value to string>".into(),
168    }
169}
170
171fn console_argument_from_handle_value(
172    cx: JSContext,
173    handle_value: HandleValue,
174    seen: &mut Vec<u64>,
175) -> DebuggerValue {
176    #[expect(unsafe_code)]
177    fn inner(
178        cx: JSContext,
179        handle_value: HandleValue,
180        seen: &mut Vec<u64>,
181    ) -> Result<DebuggerValue, ()> {
182        if handle_value.is_string() {
183            let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
184            let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
185            return Ok(DebuggerValue::StringValue(dom_string));
186        }
187
188        if handle_value.is_number() {
189            let number = handle_value.to_number();
190            return Ok(DebuggerValue::NumberValue(number));
191        }
192
193        if handle_value.is_boolean() {
194            let boolean = handle_value.to_boolean();
195            return Ok(DebuggerValue::BooleanValue(boolean));
196        }
197
198        if handle_value.is_object() {
199            // JS objects can create circular reference, and we want to avoid recursing infinitely
200            if seen.contains(&handle_value.asBits_) {
201                // FIXME: Handle this properly
202                return Ok(DebuggerValue::StringValue("[circular]".into()));
203            }
204
205            seen.push(handle_value.asBits_);
206            let maybe_argument_object = console_object_from_handle_value(cx, handle_value, seen);
207            let js_value = seen.pop();
208            debug_assert_eq!(js_value, Some(handle_value.asBits_));
209
210            if let Some(console_argument_object) = maybe_argument_object {
211                return Ok(DebuggerValue::ObjectValue {
212                    uuid: "".to_string(),
213                    class: "Object".to_owned(),
214                    preview: Some(console_argument_object),
215                });
216            }
217
218            return Err(());
219        }
220
221        // FIXME: Handle more complex argument types here
222        let stringified_value = stringify_handle_value(handle_value);
223
224        Ok(DebuggerValue::StringValue(stringified_value.into()))
225    }
226
227    match inner(cx, handle_value, seen) {
228        Ok(arg) => arg,
229        Err(()) => {
230            let in_realm_proof = AlreadyInRealm::assert_for_cx(cx);
231            report_pending_exception(
232                cx,
233                InRealm::Already(&in_realm_proof),
234                CanGc::deprecated_note(),
235            );
236            DebuggerValue::StringValue("<error>".into())
237        },
238    }
239}
240
241#[expect(unsafe_code)]
242fn console_object_from_handle_value(
243    cx: JSContext,
244    handle_value: HandleValue,
245    seen: &mut Vec<u64>,
246) -> Option<ObjectPreview> {
247    rooted!(in(*cx) let object = handle_value.to_object());
248    let mut object_class = ESClass::Other;
249    if !unsafe { GetBuiltinClass(*cx, object.handle(), &mut object_class as *mut _) } {
250        return None;
251    }
252    if object_class != ESClass::Object {
253        return None;
254    }
255
256    let mut own_properties = Vec::new();
257    let mut ids = unsafe { IdVector::new(*cx) };
258    if !unsafe {
259        GetPropertyKeys(
260            *cx,
261            object.handle(),
262            jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
263            ids.handle_mut(),
264        )
265    } {
266        return None;
267    }
268
269    for id in ids.iter() {
270        rooted!(in(*cx) let id = *id);
271        rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
272
273        let mut is_none = false;
274        if !unsafe {
275            JS_GetOwnPropertyDescriptorById(
276                *cx,
277                object.handle(),
278                id.handle(),
279                descriptor.handle_mut(),
280                &mut is_none,
281            )
282        } {
283            return None;
284        }
285
286        rooted!(in(*cx) let mut property = UndefinedValue());
287        if !unsafe { JS_GetPropertyById(*cx, object.handle(), id.handle(), property.handle_mut()) }
288        {
289            return None;
290        }
291
292        let key = if id.is_string() {
293            rooted!(in(*cx) let mut key_value = UndefinedValue());
294            let raw_id: jsapi::HandleId = id.handle().into();
295            if !unsafe { JS_IdToValue(*cx, *raw_id.ptr, key_value.handle_mut()) } {
296                continue;
297            }
298            rooted!(in(*cx) let js_string = key_value.to_string());
299            let Some(js_string) = NonNull::new(js_string.get()) else {
300                continue;
301            };
302            unsafe { jsstr_to_string(*cx, js_string) }
303        } else {
304            continue;
305        };
306
307        own_properties.push(DevtoolsPropertyDescriptor {
308            name: key,
309            value: console_argument_from_handle_value(cx, property.handle(), seen),
310            configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
311            enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
312            writable: descriptor.hasWritable_() && descriptor.writable_(),
313            is_accessor: false,
314        });
315    }
316
317    Some(ObjectPreview {
318        kind: "Object".to_owned(),
319        own_properties_length: Some(own_properties.len() as u32),
320        own_properties: Some(own_properties),
321        function: None,
322        array_length: None,
323    })
324}
325
326#[expect(unsafe_code)]
327pub(crate) fn stringify_handle_value(message: HandleValue) -> DOMString {
328    let cx = GlobalScope::get_cx();
329    unsafe {
330        if message.is_string() {
331            let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
332            return jsstr_to_string(*cx, jsstr).into();
333        }
334        unsafe fn stringify_object_from_handle_value(
335            cx: *mut jsapi::JSContext,
336            value: HandleValue,
337            parents: Vec<u64>,
338        ) -> DOMString {
339            rooted!(in(cx) let mut obj = value.to_object());
340            let mut object_class = ESClass::Other;
341            if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
342                return DOMString::from("/* invalid */");
343            }
344            let mut ids = unsafe { IdVector::new(cx) };
345            if !unsafe {
346                GetPropertyKeys(
347                    cx,
348                    obj.handle(),
349                    jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
350                    ids.handle_mut(),
351                )
352            } {
353                return DOMString::from("/* invalid */");
354            }
355            let truncate = ids.len() > MAX_LOG_CHILDREN;
356            if object_class != ESClass::Array && object_class != ESClass::Object {
357                if truncate {
358                    return DOMString::from("…");
359                } else {
360                    return unsafe { handle_value_to_string(cx, value) };
361                }
362            }
363
364            let mut explicit_keys = object_class == ESClass::Object;
365            let mut props = Vec::with_capacity(ids.len());
366            for id in ids.iter().take(MAX_LOG_CHILDREN) {
367                rooted!(in(cx) let id = *id);
368                rooted!(in(cx) let mut desc = PropertyDescriptor::default());
369
370                let mut is_none = false;
371                if !unsafe {
372                    JS_GetOwnPropertyDescriptorById(
373                        cx,
374                        obj.handle(),
375                        id.handle(),
376                        desc.handle_mut(),
377                        &mut is_none,
378                    )
379                } {
380                    return DOMString::from("/* invalid */");
381                }
382
383                rooted!(in(cx) let mut property = UndefinedValue());
384                if !unsafe {
385                    JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
386                } {
387                    return DOMString::from("/* invalid */");
388                }
389
390                if !explicit_keys {
391                    if id.is_int() {
392                        if let Ok(id_int) = usize::try_from(id.to_int()) {
393                            explicit_keys = props.len() != id_int;
394                        } else {
395                            explicit_keys = false;
396                        }
397                    } else {
398                        explicit_keys = false;
399                    }
400                }
401                let value_string = stringify_inner(
402                    unsafe { JSContext::from_ptr(cx) },
403                    property.handle(),
404                    parents.clone(),
405                );
406                if explicit_keys {
407                    let key = if id.is_string() || id.is_symbol() || id.is_int() {
408                        rooted!(in(cx) let mut key_value = UndefinedValue());
409                        let raw_id: jsapi::HandleId = id.handle().into();
410                        if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
411                            return DOMString::from("/* invalid */");
412                        }
413                        unsafe { handle_value_to_string(cx, key_value.handle()) }
414                    } else {
415                        return DOMString::from("/* invalid */");
416                    };
417                    props.push(format!("{}: {}", key, value_string,));
418                } else {
419                    props.push(value_string.to_string());
420                }
421            }
422            if truncate {
423                props.push("…".to_string());
424            }
425            if object_class == ESClass::Array {
426                DOMString::from(format!("[{}]", itertools::join(props, ", ")))
427            } else {
428                DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
429            }
430        }
431        fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
432            if parents.len() >= MAX_LOG_DEPTH {
433                return DOMString::from("...");
434            }
435            let value_bits = value.asBits_;
436            if parents.contains(&value_bits) {
437                return DOMString::from("[circular]");
438            }
439            if value.is_undefined() {
440                // This produces a better value than "(void 0)" from JS_ValueToSource.
441                return DOMString::from("undefined");
442            } else if !value.is_object() {
443                return unsafe { handle_value_to_string(*cx, value) };
444            }
445            parents.push(value_bits);
446
447            if value.is_object() {
448                if let Some(repr) = maybe_stringify_dom_object(cx, value) {
449                    return repr;
450                }
451            }
452            unsafe { stringify_object_from_handle_value(*cx, value, parents) }
453        }
454        stringify_inner(cx, message, Vec::new())
455    }
456}
457
458#[expect(unsafe_code)]
459fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
460    // The standard object serialization is not effective for DOM objects,
461    // since their properties generally live on the prototype object.
462    // Instead, fall back to the output of JSON.stringify combined
463    // with the class name extracted from the output of toString().
464    rooted!(in(*cx) let obj = value.to_object());
465    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
466    if !is_dom_class {
467        return None;
468    }
469    rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
470    let Some(class_name) = NonNull::new(class_name.get()) else {
471        return Some("<error converting DOM object to string>".into());
472    };
473    let class_name = unsafe {
474        jsstr_to_string(*cx, class_name)
475            .replace("[object ", "")
476            .replace("]", "")
477    };
478    let mut repr = format!("{} ", class_name);
479    rooted!(in(*cx) let mut value = value.get());
480
481    #[expect(unsafe_code)]
482    unsafe extern "C" fn stringified(
483        string: *const u16,
484        len: u32,
485        data: *mut std::ffi::c_void,
486    ) -> bool {
487        let s = data as *mut String;
488        let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
489        unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
490        true
491    }
492
493    rooted!(in(*cx) let space = Int32Value(2));
494    let stringify_result = unsafe {
495        JS_Stringify(
496            *cx,
497            value.handle_mut(),
498            HandleObject::null(),
499            space.handle(),
500            Some(stringified),
501            &mut repr as *mut String as *mut _,
502        )
503    };
504    if !stringify_result {
505        return Some("<error converting DOM object to string>".into());
506    }
507    Some(repr.into())
508}
509
510/// Apply sprintf-style substitutions to console format arguments per the WHATWG Console spec.
511///
512/// If the first argument is a string, it is treated as a format string where `%s`, `%d`, `%i`,
513/// `%f`, `%o`, `%O`, and `%c` are replaced by subsequent arguments. Returns the formatted string
514/// and the index of the first argument that was not consumed by a substitution.
515///
516/// <https://console.spec.whatwg.org/#formatter>
517#[expect(unsafe_code)]
518fn apply_sprintf_substitutions(cx: JSContext, messages: &[HandleValue]) -> (String, usize) {
519    debug_assert!(!messages.is_empty() && messages[0].is_string());
520
521    let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
522    let format_string = unsafe { jsstr_to_string(*cx, js_string) };
523
524    let mut result = String::new();
525    let mut arg_index = 1usize;
526    let mut chars = format_string.chars().peekable();
527
528    while let Some(c) = chars.next() {
529        if c != '%' {
530            result.push(c);
531            continue;
532        }
533
534        match chars.peek().copied() {
535            Some('s') => {
536                chars.next();
537                if arg_index < messages.len() {
538                    result.push_str(&stringify_handle_value(messages[arg_index]).to_string());
539                    arg_index += 1;
540                } else {
541                    result.push_str("%s");
542                }
543            },
544            Some('d') | Some('i') => {
545                let spec = chars.next().unwrap();
546                if arg_index < messages.len() {
547                    let num = unsafe { ToNumber(*cx, messages[arg_index]) };
548                    if num.is_err() {
549                        unsafe { jsapi::JS_ClearPendingException(*cx) };
550                    }
551                    arg_index += 1;
552                    format_integer_substitution(&mut result, num);
553                } else {
554                    result.push('%');
555                    result.push(spec);
556                }
557            },
558            Some('f') => {
559                chars.next();
560                if arg_index < messages.len() {
561                    let num = unsafe { ToNumber(*cx, messages[arg_index]) };
562                    if num.is_err() {
563                        unsafe { jsapi::JS_ClearPendingException(*cx) };
564                    }
565                    arg_index += 1;
566                    format_float_substitution(&mut result, num);
567                } else {
568                    result.push_str("%f");
569                }
570            },
571            Some('o') | Some('O') => {
572                let spec = chars.next().unwrap();
573                if arg_index < messages.len() {
574                    result.push_str(&stringify_handle_value(messages[arg_index]).to_string());
575                    arg_index += 1;
576                } else {
577                    result.push('%');
578                    result.push(spec);
579                }
580            },
581            Some('c') => {
582                chars.next();
583                if arg_index < messages.len() {
584                    arg_index += 1; // consume but ignore CSS styling
585                }
586            },
587            Some('%') => {
588                chars.next();
589                result.push('%');
590            },
591            _ => {
592                result.push('%');
593            },
594        }
595    }
596
597    (result, arg_index)
598}
599
600fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
601    match num {
602        Ok(n) if n.is_nan() => result.push_str("NaN"),
603        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
604        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
605        Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
606        Err(_) => result.push_str("NaN"),
607    }
608}
609
610fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
611    match num {
612        Ok(n) if n.is_nan() => result.push_str("NaN"),
613        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
614        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
615        Ok(n) => result.push_str(&n.to_string()),
616        Err(_) => result.push_str("NaN"),
617    }
618}
619
620fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
621    DOMString::from(itertools::join(
622        messages.iter().copied().map(stringify_handle_value),
623        " ",
624    ))
625}
626
627#[derive(Debug, Eq, PartialEq)]
628enum IncludeStackTrace {
629    Yes,
630    No,
631}
632
633impl consoleMethods<crate::DomTypeHolder> for Console {
634    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/log>
635    fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
636        Console::method(
637            global,
638            ConsoleLogLevel::Log,
639            messages,
640            IncludeStackTrace::No,
641        );
642    }
643
644    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/clear>
645    fn Clear(global: &GlobalScope) {
646        if let Some(chan) = global.devtools_chan() {
647            let worker_id = global
648                .downcast::<WorkerGlobalScope>()
649                .map(|worker| worker.worker_id());
650            let devtools_message =
651                ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
652            if let Err(error) = chan.send(devtools_message) {
653                log::warn!("Error sending clear message to devtools: {error:?}");
654            }
655        }
656    }
657
658    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
659    fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
660        Console::method(
661            global,
662            ConsoleLogLevel::Debug,
663            messages,
664            IncludeStackTrace::No,
665        );
666    }
667
668    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/info>
669    fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
670        Console::method(
671            global,
672            ConsoleLogLevel::Info,
673            messages,
674            IncludeStackTrace::No,
675        );
676    }
677
678    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/warn>
679    fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
680        Console::method(
681            global,
682            ConsoleLogLevel::Warn,
683            messages,
684            IncludeStackTrace::No,
685        );
686    }
687
688    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/error>
689    fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
690        Console::method(
691            global,
692            ConsoleLogLevel::Error,
693            messages,
694            IncludeStackTrace::No,
695        );
696    }
697
698    /// <https://console.spec.whatwg.org/#trace>
699    fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
700        Console::method(
701            global,
702            ConsoleLogLevel::Trace,
703            messages,
704            IncludeStackTrace::Yes,
705        );
706    }
707
708    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/assert>
709    fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
710        if !condition {
711            let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
712
713            Console::send_string_message(global, ConsoleLogLevel::Log, message);
714        }
715    }
716
717    /// <https://console.spec.whatwg.org/#time>
718    fn Time(global: &GlobalScope, label: DOMString) {
719        if let Ok(()) = global.time(label.clone()) {
720            let message = format!("{label}: timer started");
721            Console::send_string_message(global, ConsoleLogLevel::Log, message);
722        }
723    }
724
725    /// <https://console.spec.whatwg.org/#timelog>
726    fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
727        if let Ok(delta) = global.time_log(&label) {
728            let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
729
730            Console::send_string_message(global, ConsoleLogLevel::Log, message);
731        }
732    }
733
734    /// <https://console.spec.whatwg.org/#timeend>
735    fn TimeEnd(global: &GlobalScope, label: DOMString) {
736        if let Ok(delta) = global.time_end(&label) {
737            let message = format!("{label}: {delta}ms");
738
739            Console::send_string_message(global, ConsoleLogLevel::Log, message);
740        }
741    }
742
743    /// <https://console.spec.whatwg.org/#group>
744    fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
745        global.push_console_group(stringify_handle_values(&messages));
746    }
747
748    /// <https://console.spec.whatwg.org/#groupcollapsed>
749    fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
750        global.push_console_group(stringify_handle_values(&messages));
751    }
752
753    /// <https://console.spec.whatwg.org/#groupend>
754    fn GroupEnd(global: &GlobalScope) {
755        global.pop_console_group();
756    }
757
758    /// <https://console.spec.whatwg.org/#count>
759    fn Count(global: &GlobalScope, label: DOMString) {
760        let count = global.increment_console_count(&label);
761        let message = format!("{label}: {count}");
762
763        Console::send_string_message(global, ConsoleLogLevel::Log, message);
764    }
765
766    /// <https://console.spec.whatwg.org/#countreset>
767    fn CountReset(global: &GlobalScope, label: DOMString) {
768        if global.reset_console_count(&label).is_err() {
769            Self::internal_warn(global, format!("Counter “{label}” doesn’t exist."))
770        }
771    }
772}
773
774#[expect(unsafe_code)]
775fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
776    const MAX_FRAME_COUNT: u32 = 128;
777
778    let mut frames = vec![];
779    rooted!(in(cx) let mut handle =  ptr::null_mut());
780    let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
781    let Some(captured_js_stack) = captured_js_stack else {
782        return frames;
783    };
784
785    captured_js_stack.for_each_stack_frame(|frame| {
786        rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
787
788        // Get function name
789        unsafe {
790            jsapi::GetSavedFrameFunctionDisplayName(
791                cx,
792                ptr::null_mut(),
793                frame.into(),
794                result.handle_mut().into(),
795                jsapi::SavedFrameSelfHosted::Include,
796            );
797        }
798        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
799            unsafe { jsstr_to_string(cx, nonnull_result) }
800        } else {
801            "<anonymous>".into()
802        };
803
804        // Get source file name
805        result.set(ptr::null_mut());
806        unsafe {
807            jsapi::GetSavedFrameSource(
808                cx,
809                ptr::null_mut(),
810                frame.into(),
811                result.handle_mut().into(),
812                jsapi::SavedFrameSelfHosted::Include,
813            );
814        }
815        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
816            unsafe { jsstr_to_string(cx, nonnull_result) }
817        } else {
818            "<anonymous>".into()
819        };
820
821        // get line/column number
822        let mut line_number = 0;
823        unsafe {
824            jsapi::GetSavedFrameLine(
825                cx,
826                ptr::null_mut(),
827                frame.into(),
828                &mut line_number,
829                jsapi::SavedFrameSelfHosted::Include,
830            );
831        }
832
833        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
834        unsafe {
835            jsapi::GetSavedFrameColumn(
836                cx,
837                ptr::null_mut(),
838                frame.into(),
839                &mut column_number,
840                jsapi::SavedFrameSelfHosted::Include,
841            );
842        }
843        let frame = StackFrame {
844            filename,
845            function_name,
846            line_number,
847            column_number: column_number.value_,
848        };
849
850        frames.push(frame);
851    });
852
853    frames
854}