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::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
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 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 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 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 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 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 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 _ => ("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 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 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#[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; }
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 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 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 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 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 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 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 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 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 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 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 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 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
816 global.push_console_group(stringify_handle_values(&messages));
817 }
818
819 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
821 global.push_console_group(stringify_handle_values(&messages));
822 }
823
824 fn GroupEnd(global: &GlobalScope) {
826 global.pop_console_group();
827 }
828
829 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 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 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 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 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}