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