1use 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
40const MAX_LOG_DEPTH: usize = 10;
42const MAX_LOG_CHILDREN: usize = 15;
44
45#[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 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 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 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 if seen.contains(&handle_value.asBits_) {
204 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 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 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 _ => ("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 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 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#[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; }
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 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 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 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 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 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 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 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 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 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 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 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 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
828 global.push_console_group(stringify_handle_values(&messages));
829 }
830
831 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
833 global.push_console_group(stringify_handle_values(&messages));
834 }
835
836 fn GroupEnd(global: &GlobalScope) {
838 global.pop_console_group();
839 }
840
841 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 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 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 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 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}