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