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::rust::wrappers::{
22    GetArrayLength, GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById,
23    JS_GetPropertyById, JS_IdToValue, JS_Stringify, JS_ValueToSource,
24};
25use js::rust::{
26    CapturedJSStack, HandleObject, HandleValue, IdVector, ToNumber, ToString,
27    describe_scripted_caller,
28};
29use script_bindings::conversions::get_dom_class;
30
31use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
32use crate::dom::bindings::error::report_pending_exception;
33use crate::dom::bindings::inheritance::Castable;
34use crate::dom::bindings::str::DOMString;
35use crate::dom::globalscope::GlobalScope;
36use crate::dom::workerglobalscope::WorkerGlobalScope;
37use crate::realms::{AlreadyInRealm, InRealm};
38use crate::script_runtime::{CanGc, 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    fn method(
86        global: &GlobalScope,
87        level: ConsoleLogLevel,
88        messages: Vec<HandleValue>,
89        include_stacktrace: IncludeStackTrace,
90    ) {
91        let cx = GlobalScope::get_cx();
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, &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(*GlobalScope::get_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: JSContext,
176    handle_value: HandleValue,
177    seen: &mut Vec<u64>,
178) -> DebuggerValue {
179    #[expect(unsafe_code)]
180    fn inner(
181        cx: 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, 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            let in_realm_proof = AlreadyInRealm::assert_for_cx(cx);
234            report_pending_exception(
235                cx,
236                InRealm::Already(&in_realm_proof),
237                CanGc::deprecated_note(),
238            );
239            DebuggerValue::StringValue("<error>".into())
240        },
241    }
242}
243
244#[expect(unsafe_code)]
245fn console_object_from_handle_value(
246    cx: JSContext,
247    handle_value: HandleValue,
248    seen: &mut Vec<u64>,
249) -> Option<(String, ObjectPreview)> {
250    rooted!(in(*cx) let object = handle_value.to_object());
251    let mut object_class = ESClass::Other;
252    if !unsafe { GetBuiltinClass(*cx, object.handle(), &mut object_class as *mut _) } {
253        return None;
254    }
255    if object_class != ESClass::Object &&
256        object_class != ESClass::Array &&
257        object_class != ESClass::Function
258    {
259        return None;
260    }
261
262    let mut own_properties = Vec::new();
263    let mut items: Vec<(i32, DebuggerValue)> = Vec::new();
264    let mut ids = unsafe { IdVector::new(*cx) };
265    if !unsafe {
266        GetPropertyKeys(
267            *cx,
268            object.handle(),
269            jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
270            ids.handle_mut(),
271        )
272    } {
273        return None;
274    }
275
276    for id in ids.iter() {
277        rooted!(in(*cx) let id = *id);
278        rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
279
280        let mut is_none = false;
281        if !unsafe {
282            JS_GetOwnPropertyDescriptorById(
283                *cx,
284                object.handle(),
285                id.handle(),
286                descriptor.handle_mut(),
287                &mut is_none,
288            )
289        } {
290            return None;
291        }
292
293        rooted!(in(*cx) let mut property = UndefinedValue());
294        if !unsafe { JS_GetPropertyById(*cx, object.handle(), id.handle(), property.handle_mut()) }
295        {
296            return None;
297        }
298
299        if object_class == ESClass::Array && id.is_int() {
300            let index = id.to_int();
301            let value = console_argument_from_handle_value(cx, property.handle(), seen);
302            items.push((index, value));
303            continue;
304        }
305
306        let key = if id.is_string() {
307            rooted!(in(*cx) let mut key_value = UndefinedValue());
308            let raw_id: jsapi::HandleId = id.handle().into();
309            if !unsafe { JS_IdToValue(*cx, *raw_id.ptr, key_value.handle_mut()) } {
310                continue;
311            }
312            rooted!(in(*cx) let js_string = key_value.to_string());
313            let Some(js_string) = NonNull::new(js_string.get()) else {
314                continue;
315            };
316            unsafe { jsstr_to_string(*cx, js_string) }
317        } else {
318            continue;
319        };
320
321        own_properties.push(DevtoolsPropertyDescriptor {
322            name: key,
323            value: console_argument_from_handle_value(cx, property.handle(), seen),
324            configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
325            enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
326            writable: descriptor.hasWritable_() && descriptor.writable_(),
327            is_accessor: false,
328        });
329    }
330
331    let (class, kind, function, array_length, items) = match object_class {
332        ESClass::Array => {
333            let mut len = 0u32;
334            if !unsafe { GetArrayLength(*cx, object.handle(), &mut len) } {
335                return None;
336            }
337            items.sort_by_key(|(index, _)| *index);
338            let ordered: Vec<DebuggerValue> = items.into_iter().map(|(_, value)| value).collect();
339            (
340                "Array".into(),
341                "ArrayLike".into(),
342                None,
343                Some(len),
344                Some(ordered),
345            )
346        },
347        ESClass::Function => {
348            rooted!(in(*cx) let fun = unsafe { JS_ValueToFunction(*cx, handle_value.into()) });
349            rooted!(in(*cx) let mut name = std::ptr::null_mut::<jsapi::JSString>());
350            rooted!(in(*cx) let mut display_name = std::ptr::null_mut::<jsapi::JSString>());
351            let arity;
352            unsafe {
353                JS_GetFunctionId(*cx, fun.handle().into(), name.handle_mut().into());
354                JS_GetFunctionDisplayId(*cx, fun.handle().into(), display_name.handle_mut().into());
355                arity = JS_GetFunctionArity(fun.get());
356            }
357            let name = ptr::NonNull::new(*name).map(|name| unsafe { jsstr_to_string(*cx, name) });
358            let display_name = ptr::NonNull::new(*display_name)
359                .map(|display_name| unsafe { jsstr_to_string(*cx, display_name) });
360
361            // TODO: We should get the actual argument names from the function
362            // It's not trivial since we can't access the debugger API here
363            let parameter_names = (0..arity).map(|i| format!("<arg{i}>")).collect();
364
365            let function = FunctionPreview {
366                name,
367                display_name,
368                parameter_names,
369                is_async: None,
370                is_generator: None,
371            };
372            (
373                "Function".into(),
374                "Object".into(),
375                Some(function),
376                None,
377                None,
378            )
379        },
380        // TODO: Investigate if this class should be the object class
381        _ => ("Object".into(), "Object".into(), None, None, None),
382    };
383
384    Some((
385        class,
386        ObjectPreview {
387            kind,
388            own_properties_length: Some(own_properties.len() as u32),
389            own_properties: Some(own_properties),
390            function,
391            array_length,
392            items,
393        },
394    ))
395}
396
397#[expect(unsafe_code)]
398pub(crate) fn stringify_handle_value(message: HandleValue) -> DOMString {
399    let cx = GlobalScope::get_cx();
400    unsafe {
401        if message.is_string() {
402            let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
403            return jsstr_to_string(*cx, jsstr).into();
404        }
405        unsafe fn stringify_object_from_handle_value(
406            cx: *mut jsapi::JSContext,
407            value: HandleValue,
408            parents: Vec<u64>,
409        ) -> DOMString {
410            rooted!(in(cx) let mut obj = value.to_object());
411            let mut object_class = ESClass::Other;
412            if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
413                return DOMString::from("/* invalid */");
414            }
415            let mut ids = unsafe { IdVector::new(cx) };
416            if !unsafe {
417                GetPropertyKeys(
418                    cx,
419                    obj.handle(),
420                    jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
421                    ids.handle_mut(),
422                )
423            } {
424                return DOMString::from("/* invalid */");
425            }
426            let truncate = ids.len() > MAX_LOG_CHILDREN;
427            if object_class != ESClass::Array && object_class != ESClass::Object {
428                if truncate {
429                    return DOMString::from("…");
430                } else {
431                    return unsafe { handle_value_to_string(cx, value) };
432                }
433            }
434
435            let mut explicit_keys = object_class == ESClass::Object;
436            let mut props = Vec::with_capacity(ids.len());
437            for id in ids.iter().take(MAX_LOG_CHILDREN) {
438                rooted!(in(cx) let id = *id);
439                rooted!(in(cx) let mut desc = PropertyDescriptor::default());
440
441                let mut is_none = false;
442                if !unsafe {
443                    JS_GetOwnPropertyDescriptorById(
444                        cx,
445                        obj.handle(),
446                        id.handle(),
447                        desc.handle_mut(),
448                        &mut is_none,
449                    )
450                } {
451                    return DOMString::from("/* invalid */");
452                }
453
454                rooted!(in(cx) let mut property = UndefinedValue());
455                if !unsafe {
456                    JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
457                } {
458                    return DOMString::from("/* invalid */");
459                }
460
461                if !explicit_keys {
462                    if id.is_int() {
463                        if let Ok(id_int) = usize::try_from(id.to_int()) {
464                            explicit_keys = props.len() != id_int;
465                        } else {
466                            explicit_keys = false;
467                        }
468                    } else {
469                        explicit_keys = false;
470                    }
471                }
472                let value_string = stringify_inner(
473                    unsafe { JSContext::from_ptr(cx) },
474                    property.handle(),
475                    parents.clone(),
476                );
477                if explicit_keys {
478                    let key = if id.is_string() || id.is_symbol() || id.is_int() {
479                        rooted!(in(cx) let mut key_value = UndefinedValue());
480                        let raw_id: jsapi::HandleId = id.handle().into();
481                        if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
482                            return DOMString::from("/* invalid */");
483                        }
484                        unsafe { handle_value_to_string(cx, key_value.handle()) }
485                    } else {
486                        return DOMString::from("/* invalid */");
487                    };
488                    props.push(format!("{}: {}", key, value_string,));
489                } else {
490                    props.push(value_string.to_string());
491                }
492            }
493            if truncate {
494                props.push("…".to_string());
495            }
496            if object_class == ESClass::Array {
497                DOMString::from(format!("[{}]", itertools::join(props, ", ")))
498            } else {
499                DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
500            }
501        }
502        fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
503            if parents.len() >= MAX_LOG_DEPTH {
504                return DOMString::from("...");
505            }
506            let value_bits = value.asBits_;
507            if parents.contains(&value_bits) {
508                return DOMString::from("[circular]");
509            }
510            if value.is_undefined() {
511                // This produces a better value than "(void 0)" from JS_ValueToSource.
512                return DOMString::from("undefined");
513            } else if !value.is_object() {
514                return unsafe { handle_value_to_string(*cx, value) };
515            }
516            parents.push(value_bits);
517
518            if value.is_object() {
519                if let Some(repr) = maybe_stringify_dom_object(cx, value) {
520                    return repr;
521                }
522            }
523            unsafe { stringify_object_from_handle_value(*cx, value, parents) }
524        }
525        stringify_inner(cx, message, Vec::new())
526    }
527}
528
529#[expect(unsafe_code)]
530fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
531    // The standard object serialization is not effective for DOM objects,
532    // since their properties generally live on the prototype object.
533    // Instead, fall back to the output of JSON.stringify combined
534    // with the class name extracted from the output of toString().
535    rooted!(in(*cx) let obj = value.to_object());
536    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
537    if !is_dom_class {
538        return None;
539    }
540    rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
541    let Some(class_name) = NonNull::new(class_name.get()) else {
542        return Some("<error converting DOM object to string>".into());
543    };
544    let class_name = unsafe {
545        jsstr_to_string(*cx, class_name)
546            .replace("[object ", "")
547            .replace("]", "")
548    };
549    let mut repr = format!("{} ", class_name);
550    rooted!(in(*cx) let mut value = value.get());
551
552    #[expect(unsafe_code)]
553    unsafe extern "C" fn stringified(
554        string: *const u16,
555        len: u32,
556        data: *mut std::ffi::c_void,
557    ) -> bool {
558        let s = data as *mut String;
559        let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
560        unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
561        true
562    }
563
564    rooted!(in(*cx) let space = Int32Value(2));
565    let stringify_result = unsafe {
566        JS_Stringify(
567            *cx,
568            value.handle_mut(),
569            HandleObject::null(),
570            space.handle(),
571            Some(stringified),
572            &mut repr as *mut String as *mut _,
573        )
574    };
575    if !stringify_result {
576        return Some("<error converting DOM object to string>".into());
577    }
578    Some(repr.into())
579}
580
581/// Apply sprintf-style substitutions to console format arguments per the WHATWG Console spec.
582///
583/// If the first argument is a string, it is treated as a format string where `%s`, `%d`, `%i`,
584/// `%f`, `%o`, `%O`, and `%c` are replaced by subsequent arguments. Returns the formatted string
585/// and the index of the first argument that was not consumed by a substitution.
586///
587/// <https://console.spec.whatwg.org/#formatter>
588#[expect(unsafe_code)]
589fn apply_sprintf_substitutions(cx: JSContext, messages: &[HandleValue]) -> (String, usize) {
590    debug_assert!(!messages.is_empty() && messages[0].is_string());
591
592    let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
593    let format_string = unsafe { jsstr_to_string(*cx, js_string) };
594
595    let mut result = String::new();
596    let mut arg_index = 1usize;
597    let mut chars = format_string.chars().peekable();
598
599    while let Some(c) = chars.next() {
600        if c != '%' {
601            result.push(c);
602            continue;
603        }
604
605        match chars.peek().copied() {
606            Some('s') => {
607                chars.next();
608                if arg_index < messages.len() {
609                    result.push_str(&stringify_handle_value(messages[arg_index]).to_string());
610                    arg_index += 1;
611                } else {
612                    result.push_str("%s");
613                }
614            },
615            Some('d') | Some('i') => {
616                let spec = chars.next().unwrap();
617                if arg_index < messages.len() {
618                    let num = unsafe { ToNumber(*cx, messages[arg_index]) };
619                    if num.is_err() {
620                        unsafe { jsapi::JS_ClearPendingException(*cx) };
621                    }
622                    arg_index += 1;
623                    format_integer_substitution(&mut result, num);
624                } else {
625                    result.push('%');
626                    result.push(spec);
627                }
628            },
629            Some('f') => {
630                chars.next();
631                if arg_index < messages.len() {
632                    let num = unsafe { ToNumber(*cx, messages[arg_index]) };
633                    if num.is_err() {
634                        unsafe { jsapi::JS_ClearPendingException(*cx) };
635                    }
636                    arg_index += 1;
637                    format_float_substitution(&mut result, num);
638                } else {
639                    result.push_str("%f");
640                }
641            },
642            Some('o') | Some('O') => {
643                let spec = chars.next().unwrap();
644                if arg_index < messages.len() {
645                    result.push_str(&stringify_handle_value(messages[arg_index]).to_string());
646                    arg_index += 1;
647                } else {
648                    result.push('%');
649                    result.push(spec);
650                }
651            },
652            Some('c') => {
653                chars.next();
654                if arg_index < messages.len() {
655                    arg_index += 1; // consume but ignore CSS styling
656                }
657            },
658            Some('%') => {
659                chars.next();
660                result.push('%');
661            },
662            _ => {
663                result.push('%');
664            },
665        }
666    }
667
668    (result, arg_index)
669}
670
671fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
672    match num {
673        Ok(n) if n.is_nan() => result.push_str("NaN"),
674        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
675        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
676        Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
677        Err(_) => result.push_str("NaN"),
678    }
679}
680
681fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
682    match num {
683        Ok(n) if n.is_nan() => result.push_str("NaN"),
684        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
685        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
686        Ok(n) => result.push_str(&n.to_string()),
687        Err(_) => result.push_str("NaN"),
688    }
689}
690
691fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
692    DOMString::from(itertools::join(
693        messages.iter().copied().map(stringify_handle_value),
694        " ",
695    ))
696}
697
698#[derive(Debug, Eq, PartialEq)]
699enum IncludeStackTrace {
700    Yes,
701    No,
702}
703
704impl consoleMethods<crate::DomTypeHolder> for Console {
705    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/log>
706    fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
707        Console::method(
708            global,
709            ConsoleLogLevel::Log,
710            messages,
711            IncludeStackTrace::No,
712        );
713    }
714
715    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/clear>
716    fn Clear(global: &GlobalScope) {
717        if let Some(chan) = global.devtools_chan() {
718            let worker_id = global
719                .downcast::<WorkerGlobalScope>()
720                .map(|worker| worker.worker_id());
721            let devtools_message =
722                ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
723            if let Err(error) = chan.send(devtools_message) {
724                log::warn!("Error sending clear message to devtools: {error:?}");
725            }
726        }
727    }
728
729    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
730    fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
731        Console::method(
732            global,
733            ConsoleLogLevel::Debug,
734            messages,
735            IncludeStackTrace::No,
736        );
737    }
738
739    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/info>
740    fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
741        Console::method(
742            global,
743            ConsoleLogLevel::Info,
744            messages,
745            IncludeStackTrace::No,
746        );
747    }
748
749    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/warn>
750    fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
751        Console::method(
752            global,
753            ConsoleLogLevel::Warn,
754            messages,
755            IncludeStackTrace::No,
756        );
757    }
758
759    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/error>
760    fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
761        Console::method(
762            global,
763            ConsoleLogLevel::Error,
764            messages,
765            IncludeStackTrace::No,
766        );
767    }
768
769    /// <https://console.spec.whatwg.org/#trace>
770    fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
771        Console::method(
772            global,
773            ConsoleLogLevel::Trace,
774            messages,
775            IncludeStackTrace::Yes,
776        );
777    }
778
779    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/assert>
780    fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
781        if !condition {
782            let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
783
784            Console::send_string_message(global, ConsoleLogLevel::Log, message);
785        }
786    }
787
788    /// <https://console.spec.whatwg.org/#time>
789    fn Time(global: &GlobalScope, label: DOMString) {
790        if let Ok(()) = global.time(label.clone()) {
791            let message = format!("{label}: timer started");
792            Console::send_string_message(global, ConsoleLogLevel::Log, message);
793        }
794    }
795
796    /// <https://console.spec.whatwg.org/#timelog>
797    fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
798        if let Ok(delta) = global.time_log(&label) {
799            let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
800
801            Console::send_string_message(global, ConsoleLogLevel::Log, message);
802        }
803    }
804
805    /// <https://console.spec.whatwg.org/#timeend>
806    fn TimeEnd(global: &GlobalScope, label: DOMString) {
807        if let Ok(delta) = global.time_end(&label) {
808            let message = format!("{label}: {delta}ms");
809
810            Console::send_string_message(global, ConsoleLogLevel::Log, message);
811        }
812    }
813
814    /// <https://console.spec.whatwg.org/#group>
815    fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
816        global.push_console_group(stringify_handle_values(&messages));
817    }
818
819    /// <https://console.spec.whatwg.org/#groupcollapsed>
820    fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
821        global.push_console_group(stringify_handle_values(&messages));
822    }
823
824    /// <https://console.spec.whatwg.org/#groupend>
825    fn GroupEnd(global: &GlobalScope) {
826        global.pop_console_group();
827    }
828
829    /// <https://console.spec.whatwg.org/#count>
830    fn Count(global: &GlobalScope, label: DOMString) {
831        let count = global.increment_console_count(&label);
832        let message = format!("{label}: {count}");
833
834        Console::send_string_message(global, ConsoleLogLevel::Log, message);
835    }
836
837    /// <https://console.spec.whatwg.org/#countreset>
838    fn CountReset(global: &GlobalScope, label: DOMString) {
839        if global.reset_console_count(&label).is_err() {
840            Self::internal_warn(global, format!("Counter “{label}” doesn’t exist."))
841        }
842    }
843}
844
845#[expect(unsafe_code)]
846fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
847    const MAX_FRAME_COUNT: u32 = 128;
848
849    let mut frames = vec![];
850    rooted!(in(cx) let mut handle =  ptr::null_mut());
851    let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
852    let Some(captured_js_stack) = captured_js_stack else {
853        return frames;
854    };
855
856    captured_js_stack.for_each_stack_frame(|frame| {
857        rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
858
859        // Get function name
860        unsafe {
861            jsapi::GetSavedFrameFunctionDisplayName(
862                cx,
863                ptr::null_mut(),
864                frame.into(),
865                result.handle_mut().into(),
866                jsapi::SavedFrameSelfHosted::Include,
867            );
868        }
869        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
870            unsafe { jsstr_to_string(cx, nonnull_result) }
871        } else {
872            "<anonymous>".into()
873        };
874
875        // Get source file name
876        result.set(ptr::null_mut());
877        unsafe {
878            jsapi::GetSavedFrameSource(
879                cx,
880                ptr::null_mut(),
881                frame.into(),
882                result.handle_mut().into(),
883                jsapi::SavedFrameSelfHosted::Include,
884            );
885        }
886        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
887            unsafe { jsstr_to_string(cx, nonnull_result) }
888        } else {
889            "<anonymous>".into()
890        };
891
892        // get line/column number
893        let mut line_number = 0;
894        unsafe {
895            jsapi::GetSavedFrameLine(
896                cx,
897                ptr::null_mut(),
898                frame.into(),
899                &mut line_number,
900                jsapi::SavedFrameSelfHosted::Include,
901            );
902        }
903
904        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
905        unsafe {
906            jsapi::GetSavedFrameColumn(
907                cx,
908                ptr::null_mut(),
909                frame.into(),
910                &mut column_number,
911                jsapi::SavedFrameSelfHosted::Include,
912            );
913        }
914        let frame = StackFrame {
915            filename,
916            function_name,
917            line_number,
918            column_number: column_number.value_,
919        };
920
921        frames.push(frame);
922    });
923
924    frames
925}