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::context::JSContext;
16use js::conversions::jsstr_to_string;
17use js::jsapi::{self, ESClass, JS_GetFunctionArity, PropertyDescriptor, SavedFrameSelfHosted};
18use js::jsval::{Int32Value, UndefinedValue};
19use js::realm::CurrentRealm;
20use js::rust::wrappers2::{
21 GetArrayLength, GetBuiltinClass, GetPropertyKeys, GetSavedFrameColumn,
22 GetSavedFrameFunctionDisplayName, GetSavedFrameLine, GetSavedFrameSource,
23 JS_ClearPendingException, JS_GetFunctionDisplayId, JS_GetFunctionId,
24 JS_GetOwnPropertyDescriptorById, JS_GetPropertyById, JS_IdToValue, JS_Stringify,
25 JS_ValueToFunction, JS_ValueToSource,
26};
27use js::rust::{
28 CapturedJSStack, HandleObject, HandleValue, IdVector, ToNumber, ToString,
29 describe_scripted_caller,
30};
31use script_bindings::conversions::get_dom_class;
32
33use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
34use crate::dom::bindings::error::report_pending_exception;
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::str::DOMString;
37use crate::dom::globalscope::GlobalScope;
38use crate::dom::workerglobalscope::WorkerGlobalScope;
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 cx: &mut JSContext,
53 level: ConsoleLogLevel,
54 arguments: Vec<DebuggerValue>,
55 stacktrace: Option<Vec<StackFrame>>,
56 ) -> ConsoleMessage {
57 let caller = unsafe { describe_scripted_caller(cx.raw_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(
74 cx: &mut JSContext,
75 global: &GlobalScope,
76 level: ConsoleLogLevel,
77 message: String,
78 ) {
79 let prefix = global.current_group_label().unwrap_or_default();
80 let formatted_message = format!("{prefix}{message}");
81
82 Self::send_to_embedder(global, level.clone(), formatted_message);
83
84 let console_message =
85 Self::build_message(cx, level, vec![DebuggerValue::StringValue(message)], None);
86
87 Self::send_to_devtools(global, console_message);
88 }
89
90 fn method(
91 cx: &mut JSContext,
92 global: &GlobalScope,
93 level: ConsoleLogLevel,
94 messages: Vec<HandleValue>,
95 include_stacktrace: IncludeStackTrace,
96 ) {
97 let (arguments, embedder_msg) = if !messages.is_empty() && messages[0].is_string() {
101 let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
102 let remaining = &messages[consumed..];
103
104 let mut arguments: Vec<DebuggerValue> =
105 vec![DebuggerValue::StringValue(formatted.clone())];
106 for msg in remaining {
107 arguments.push(console_argument_from_handle_value(
108 cx,
109 *msg,
110 &mut Vec::new(),
111 ));
112 }
113
114 let embedder_msg = if remaining.is_empty() {
115 formatted
116 } else {
117 format!("{formatted} {}", stringify_handle_values(cx, remaining))
118 };
119
120 (arguments, embedder_msg.into())
121 } else {
122 let arguments = messages
123 .iter()
124 .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
125 .collect();
126 (arguments, stringify_handle_values(cx, &messages))
127 };
128
129 let stacktrace = (include_stacktrace == IncludeStackTrace::Yes).then_some(get_js_stack(cx));
130 let console_message = Self::build_message(cx, level.clone(), arguments, stacktrace);
131
132 Console::send_to_devtools(global, console_message);
133
134 let prefix = global.current_group_label().unwrap_or_default();
135 let formatted_message = format!("{prefix}{embedder_msg}");
136
137 Self::send_to_embedder(global, level, formatted_message);
138 }
139
140 fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
141 if let Some(chan) = global.devtools_chan() {
142 let worker_id = global
143 .downcast::<WorkerGlobalScope>()
144 .map(|worker| worker.worker_id());
145 let devtools_message =
146 ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
147 chan.send(devtools_message).unwrap();
148 }
149 }
150
151 fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
152 global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
153 global.webview_id(),
154 level,
155 message,
156 ));
157 }
158
159 pub(crate) fn internal_warn(cx: &mut JSContext, global: &GlobalScope, message: String) {
161 Console::send_string_message(cx, global, ConsoleLogLevel::Warn, message);
162 }
163}
164
165#[expect(unsafe_code)]
166fn handle_value_to_string(cx: &mut JSContext, value: HandleValue) -> DOMString {
167 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
168 Some(js_str) => unsafe { jsstr_to_string(cx, js_str) }.into(),
169 None => "<error converting value to string>".into(),
170 }
171}
172
173fn console_argument_from_handle_value(
174 cx: &mut JSContext,
175 handle_value: HandleValue,
176 seen: &mut Vec<u64>,
177) -> DebuggerValue {
178 #[expect(unsafe_code)]
179 fn inner(
180 cx: &mut JSContext,
181 handle_value: HandleValue,
182 seen: &mut Vec<u64>,
183 ) -> Result<DebuggerValue, ()> {
184 if handle_value.is_string() {
185 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
186 let dom_string = unsafe { jsstr_to_string(cx, js_string) };
187 return Ok(DebuggerValue::StringValue(dom_string));
188 }
189
190 if handle_value.is_number() {
191 let number = handle_value.to_number();
192 return Ok(DebuggerValue::NumberValue(number));
193 }
194
195 if handle_value.is_boolean() {
196 let boolean = handle_value.to_boolean();
197 return Ok(DebuggerValue::BooleanValue(boolean));
198 }
199
200 if handle_value.is_object() {
201 if seen.contains(&handle_value.asBits_) {
203 return Ok(DebuggerValue::StringValue("[circular]".into()));
205 }
206
207 seen.push(handle_value.asBits_);
208 let console_object = console_object_from_handle_value(cx, handle_value, seen);
209 let js_value = seen.pop();
210 debug_assert_eq!(js_value, Some(handle_value.asBits_));
211
212 if let Some((class, preview)) = console_object {
213 return Ok(DebuggerValue::ObjectValue {
214 uuid: uuid::Uuid::new_v4().to_string(),
215 class,
216 own_property_length: preview.own_properties_length,
217 preview: Some(Box::new(preview)),
218 });
219 }
220
221 return Err(());
222 }
223
224 let stringified_value = stringify_handle_value(cx, 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
239fn accessor_value_from_property_descriptor(descriptor: &PropertyDescriptor) -> DebuggerValue {
240 let value = match (
243 descriptor.hasGetter_() && !descriptor.getter_.is_null(),
244 descriptor.hasSetter_() && !descriptor.setter_.is_null(),
245 ) {
246 (true, true) => "Getter/Setter",
247 (true, false) => "Getter",
248 (false, true) => "Setter",
249 (false, false) => "undefined",
250 };
251 DebuggerValue::StringValue(value.into())
252}
253
254#[expect(unsafe_code)]
255fn console_object_from_handle_value(
256 cx: &mut JSContext,
257 handle_value: HandleValue,
258 seen: &mut Vec<u64>,
259) -> Option<(String, ObjectPreview)> {
260 rooted!(&in(cx) let object = handle_value.to_object());
261 let mut object_class = ESClass::Other;
262 if !unsafe { GetBuiltinClass(cx, object.handle(), &mut object_class as *mut _) } {
263 return None;
264 }
265 if object_class != ESClass::Object &&
266 object_class != ESClass::Array &&
267 object_class != ESClass::Function
268 {
269 return None;
270 }
271
272 let mut own_properties = Vec::new();
273 let mut items: Vec<(i32, DebuggerValue)> = Vec::new();
274 let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
275 if !unsafe {
278 GetPropertyKeys(
279 cx,
280 object.handle(),
281 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
282 ids.handle_mut(),
283 )
284 } {
285 return None;
286 }
287
288 for id in ids.iter() {
289 rooted!(&in(cx) let id = *id);
290 rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
291
292 let mut is_none = false;
293 if !unsafe {
294 JS_GetOwnPropertyDescriptorById(
295 cx,
296 object.handle(),
297 id.handle(),
298 descriptor.handle_mut(),
299 &mut is_none,
300 )
301 } {
302 return None;
303 }
304 if is_none {
305 continue;
306 }
307
308 let is_accessor = (descriptor.hasGetter_() && !descriptor.getter_.is_null()) ||
311 (descriptor.hasSetter_() && !descriptor.setter_.is_null());
312 let value = if is_accessor {
313 accessor_value_from_property_descriptor(&descriptor)
314 } else {
315 rooted!(&in(cx) let property = descriptor.value_);
316 console_argument_from_handle_value(cx, property.handle(), seen)
317 };
318
319 if object_class == ESClass::Array && id.is_int() {
320 let index = id.to_int();
321 items.push((index, value));
322 continue;
323 }
324
325 let key = if id.is_string() {
326 rooted!(&in(cx) let mut key_value = UndefinedValue());
327 if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
328 continue;
329 }
330 rooted!(&in(cx) let js_string = key_value.to_string());
331 let Some(js_string) = NonNull::new(js_string.get()) else {
332 continue;
333 };
334 unsafe { jsstr_to_string(cx, js_string) }
335 } else if id.is_symbol() || id.is_int() {
336 rooted!(&in(cx) let mut key_value = UndefinedValue());
337 if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
338 continue;
339 }
340 handle_value_to_string(cx, key_value.handle()).to_string()
341 } else {
342 continue;
343 };
344
345 own_properties.push(DevtoolsPropertyDescriptor {
346 name: key,
347 value,
348 configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
349 enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
350 writable: !is_accessor && descriptor.hasWritable_() && descriptor.writable_(),
351 is_accessor,
352 });
353 }
354
355 let (class, kind, function, array_length, items) = match object_class {
356 ESClass::Array => {
357 let mut len = 0u32;
358 if !unsafe { GetArrayLength(cx, object.handle(), &mut len) } {
359 return None;
360 }
361 items.sort_by_key(|(index, _)| *index);
362 let ordered: Vec<DebuggerValue> = items.into_iter().map(|(_, value)| value).collect();
363 (
364 "Array".into(),
365 "ArrayLike".into(),
366 None,
367 Some(len),
368 Some(ordered),
369 )
370 },
371 ESClass::Function => {
372 rooted!(&in(cx) let fun = unsafe { JS_ValueToFunction(cx, handle_value) });
373 rooted!(&in(cx) let mut name = std::ptr::null_mut::<jsapi::JSString>());
374 rooted!(&in(cx) let mut display_name = std::ptr::null_mut::<jsapi::JSString>());
375 let arity;
376 unsafe {
377 JS_GetFunctionId(cx, fun.handle(), name.handle_mut());
378 JS_GetFunctionDisplayId(cx, fun.handle(), display_name.handle_mut());
379 arity = JS_GetFunctionArity(fun.get());
380 }
381 let name = ptr::NonNull::new(*name).map(|name| unsafe { jsstr_to_string(cx, name) });
382 let display_name = ptr::NonNull::new(*display_name)
383 .map(|display_name| unsafe { jsstr_to_string(cx, display_name) });
384
385 let parameter_names = (0..arity).map(|i| format!("<arg{i}>")).collect();
388
389 let function = FunctionPreview {
390 name,
391 display_name,
392 parameter_names,
393 is_async: None,
394 is_generator: None,
395 };
396 (
397 "Function".into(),
398 "Object".into(),
399 Some(function),
400 None,
401 None,
402 )
403 },
404 _ => ("Object".into(), "Object".into(), None, None, None),
406 };
407
408 Some((
409 class,
410 ObjectPreview {
411 kind,
412 size: None,
413 entries: None,
414 own_properties_length: Some(own_properties.len() as u32),
415 own_properties: Some(own_properties),
416 function,
417 array_length,
418 items,
419 },
420 ))
421}
422
423#[expect(unsafe_code)]
424pub(crate) fn stringify_handle_value(cx: &mut JSContext, message: HandleValue) -> DOMString {
425 if message.is_string() {
426 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
427 return unsafe { jsstr_to_string(cx, jsstr) }.into();
428 }
429 fn stringify_object_from_handle_value(
430 cx: &mut JSContext,
431 value: HandleValue,
432 parents: Vec<u64>,
433 ) -> DOMString {
434 rooted!(&in(cx) let mut obj = value.to_object());
435 let mut object_class = ESClass::Other;
436 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
437 return DOMString::from("/* invalid */");
438 }
439 let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
440 if !unsafe {
441 GetPropertyKeys(
442 cx,
443 obj.handle(),
444 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
445 ids.handle_mut(),
446 )
447 } {
448 return DOMString::from("/* invalid */");
449 }
450 let truncate = ids.len() > MAX_LOG_CHILDREN;
451 if object_class != ESClass::Array && object_class != ESClass::Object {
452 if truncate {
453 return DOMString::from("…");
454 } else {
455 return handle_value_to_string(cx, value);
456 }
457 }
458
459 let mut explicit_keys = object_class == ESClass::Object;
460 let mut props = Vec::with_capacity(ids.len());
461 for id in ids.iter().take(MAX_LOG_CHILDREN) {
462 rooted!(&in(cx) let id = *id);
463 rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
464
465 let mut is_none = false;
466 if !unsafe {
467 JS_GetOwnPropertyDescriptorById(
468 cx,
469 obj.handle(),
470 id.handle(),
471 desc.handle_mut(),
472 &mut is_none,
473 )
474 } {
475 return DOMString::from("/* invalid */");
476 }
477
478 rooted!(&in(cx) let mut property = UndefinedValue());
479 if !unsafe { JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) }
480 {
481 return DOMString::from("/* invalid */");
482 }
483
484 if !explicit_keys {
485 if id.is_int() {
486 if let Ok(id_int) = usize::try_from(id.to_int()) {
487 explicit_keys = props.len() != id_int;
488 } else {
489 explicit_keys = false;
490 }
491 } else {
492 explicit_keys = false;
493 }
494 }
495 let value_string = stringify_inner(cx, property.handle(), parents.clone());
496 if explicit_keys {
497 let key = if id.is_string() || id.is_symbol() || id.is_int() {
498 rooted!(&in(cx) let mut key_value = UndefinedValue());
499 if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
500 return DOMString::from("/* invalid */");
501 }
502 handle_value_to_string(cx, key_value.handle())
503 } else {
504 return DOMString::from("/* invalid */");
505 };
506 props.push(format!("{}: {}", key, value_string,));
507 } else {
508 props.push(String::from(value_string));
509 }
510 }
511 if truncate {
512 props.push("…".to_string());
513 }
514 if object_class == ESClass::Array {
515 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
516 } else {
517 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
518 }
519 }
520 fn stringify_inner(cx: &mut JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
521 if parents.len() >= MAX_LOG_DEPTH {
522 return DOMString::from("...");
523 }
524 let value_bits = value.asBits_;
525 if parents.contains(&value_bits) {
526 return DOMString::from("[circular]");
527 }
528 if value.is_undefined() {
529 return DOMString::from("undefined");
531 } else if !value.is_object() {
532 return handle_value_to_string(cx, value);
533 }
534 parents.push(value_bits);
535
536 if value.is_object() &&
537 let Some(repr) = maybe_stringify_dom_object(cx, value)
538 {
539 return repr;
540 }
541 stringify_object_from_handle_value(cx, value, parents)
542 }
543 stringify_inner(cx, message, Vec::new())
544}
545
546#[expect(unsafe_code)]
547fn maybe_stringify_dom_object(cx: &mut JSContext, value: HandleValue) -> Option<DOMString> {
548 rooted!(&in(cx) let obj = value.to_object());
553 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
554 if !is_dom_class {
555 return None;
556 }
557 rooted!(&in(cx) let class_name = unsafe { ToString(cx, value) });
558 let Some(class_name) = NonNull::new(class_name.get()) else {
559 return Some("<error converting DOM object to string>".into());
560 };
561 let class_name = unsafe { jsstr_to_string(cx, class_name) }
562 .replace("[object ", "")
563 .replace("]", "");
564 let mut repr = format!("{} ", class_name);
565 rooted!(&in(cx) let mut value = value.get());
566
567 #[expect(unsafe_code)]
568 unsafe extern "C" fn stringified(
569 string: *const u16,
570 len: u32,
571 data: *mut std::ffi::c_void,
572 ) -> bool {
573 let s = data as *mut String;
574 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
575 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
576 true
577 }
578
579 rooted!(&in(cx) let space = Int32Value(2));
580 let stringify_result = unsafe {
581 JS_Stringify(
582 cx,
583 value.handle_mut(),
584 HandleObject::null(),
585 space.handle(),
586 Some(stringified),
587 &mut repr as *mut String as *mut _,
588 )
589 };
590 if !stringify_result {
591 return Some("<error converting DOM object to string>".into());
592 }
593 Some(repr.into())
594}
595
596#[expect(unsafe_code)]
604fn apply_sprintf_substitutions(cx: &mut JSContext, messages: &[HandleValue]) -> (String, usize) {
605 debug_assert!(!messages.is_empty() && messages[0].is_string());
606
607 let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
608 let format_string = unsafe { jsstr_to_string(cx, js_string) };
609
610 let mut result = String::new();
611 let mut arg_index = 1usize;
612 let mut chars = format_string.chars().peekable();
613
614 while let Some(c) = chars.next() {
615 if c != '%' {
616 result.push(c);
617 continue;
618 }
619
620 match chars.peek().copied() {
621 Some('s') => {
622 chars.next();
623 if arg_index < messages.len() {
624 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
625 arg_index += 1;
626 } else {
627 result.push_str("%s");
628 }
629 },
630 Some('d') | Some('i') => {
631 let spec = chars.next().unwrap();
632 if arg_index < messages.len() {
633 let num = unsafe { ToNumber(cx.raw_cx(), messages[arg_index]) };
634 if num.is_err() {
635 unsafe { JS_ClearPendingException(cx) };
636 }
637 arg_index += 1;
638 format_integer_substitution(&mut result, num);
639 } else {
640 result.push('%');
641 result.push(spec);
642 }
643 },
644 Some('f') => {
645 chars.next();
646 if arg_index < messages.len() {
647 let num = unsafe { ToNumber(cx.raw_cx(), messages[arg_index]) };
648 if num.is_err() {
649 unsafe { JS_ClearPendingException(cx) };
650 }
651 arg_index += 1;
652 format_float_substitution(&mut result, num);
653 } else {
654 result.push_str("%f");
655 }
656 },
657 Some('o') | Some('O') => {
658 let spec = chars.next().unwrap();
659 if arg_index < messages.len() {
660 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
661 arg_index += 1;
662 } else {
663 result.push('%');
664 result.push(spec);
665 }
666 },
667 Some('c') => {
668 chars.next();
669 if arg_index < messages.len() {
670 arg_index += 1; }
672 },
673 Some('%') => {
674 chars.next();
675 result.push('%');
676 },
677 _ => {
678 result.push('%');
679 },
680 }
681 }
682
683 (result, arg_index)
684}
685
686fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
687 match num {
688 Ok(n) if n.is_nan() => result.push_str("NaN"),
689 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
690 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
691 Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
692 Err(_) => result.push_str("NaN"),
693 }
694}
695
696fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
697 match num {
698 Ok(n) if n.is_nan() => result.push_str("NaN"),
699 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
700 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
701 Ok(n) => result.push_str(&n.to_string()),
702 Err(_) => result.push_str("NaN"),
703 }
704}
705
706fn stringify_handle_values(cx: &mut JSContext, messages: &[HandleValue]) -> DOMString {
707 DOMString::from(itertools::join(
708 messages
709 .iter()
710 .copied()
711 .map(|msg| stringify_handle_value(cx, msg)),
712 " ",
713 ))
714}
715
716fn stringify_debugger_value(value: &DebuggerValue) -> String {
719 match value {
720 DebuggerValue::VoidValue => "undefined".into(),
721 DebuggerValue::NullValue => "null".into(),
722 DebuggerValue::BooleanValue(value) => value.to_string(),
723 DebuggerValue::NumberValue(value) => value.to_string(),
724 DebuggerValue::StringValue(value) => value.clone(),
725 DebuggerValue::ObjectValue { class, preview, .. } => {
726 let Some(preview) = preview else {
727 return class.clone();
728 };
729
730 if preview.kind == "ArrayLike" {
731 let mut items = preview
732 .items
733 .as_ref()
734 .map(|items| {
735 items
736 .iter()
737 .take(MAX_LOG_CHILDREN)
738 .map(stringify_debugger_value)
739 .collect::<Vec<_>>()
740 })
741 .unwrap_or_default();
742 if preview
743 .array_length
744 .is_some_and(|length| length as usize > items.len())
745 {
746 items.push("...".into());
747 }
748 return format!("[{}]", itertools::join(items, ", "));
749 }
750
751 let mut properties = preview
752 .own_properties
753 .as_ref()
754 .map(|properties| {
755 properties
756 .iter()
757 .take(MAX_LOG_CHILDREN)
758 .map(|property| {
759 format!(
760 "{}: {}",
761 property.name,
762 stringify_debugger_value(&property.value)
763 )
764 })
765 .collect::<Vec<_>>()
766 })
767 .unwrap_or_default();
768 if preview
769 .own_properties_length
770 .is_some_and(|length| length as usize > properties.len())
771 {
772 properties.push("...".into());
773 }
774 format!("{class} {{{}}}", itertools::join(properties, ", "))
775 },
776 }
777}
778
779#[derive(Debug, Eq, PartialEq)]
780enum IncludeStackTrace {
781 Yes,
782 No,
783}
784
785impl consoleMethods<crate::DomTypeHolder> for Console {
786 fn Log(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
788 Console::method(
789 cx,
790 global,
791 ConsoleLogLevel::Log,
792 messages,
793 IncludeStackTrace::No,
794 );
795 }
796
797 fn Clear(global: &GlobalScope) {
799 if let Some(chan) = global.devtools_chan() {
800 let worker_id = global
801 .downcast::<WorkerGlobalScope>()
802 .map(|worker| worker.worker_id());
803 let devtools_message =
804 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
805 if let Err(error) = chan.send(devtools_message) {
806 log::warn!("Error sending clear message to devtools: {error:?}");
807 }
808 }
809 }
810
811 fn Debug(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
813 Console::method(
814 cx,
815 global,
816 ConsoleLogLevel::Debug,
817 messages,
818 IncludeStackTrace::No,
819 );
820 }
821
822 fn Info(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
824 Console::method(
825 cx,
826 global,
827 ConsoleLogLevel::Info,
828 messages,
829 IncludeStackTrace::No,
830 );
831 }
832
833 fn Warn(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
835 Console::method(
836 cx,
837 global,
838 ConsoleLogLevel::Warn,
839 messages,
840 IncludeStackTrace::No,
841 );
842 }
843
844 fn Error(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
846 Console::method(
847 cx,
848 global,
849 ConsoleLogLevel::Error,
850 messages,
851 IncludeStackTrace::No,
852 );
853 }
854
855 fn Trace(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
857 Console::method(
858 cx,
859 global,
860 ConsoleLogLevel::Trace,
861 messages,
862 IncludeStackTrace::Yes,
863 );
864 }
865
866 fn Dir(
868 cx: &mut js::context::JSContext,
869 global: &GlobalScope,
870 item: HandleValue,
871 _options: Option<*mut jsapi::JSObject>,
872 ) {
873 let argument = console_argument_from_handle_value(cx, item, &mut Vec::new());
875 let prefix = global.current_group_label().unwrap_or_default();
876 Console::send_to_devtools(
878 global,
879 Self::build_message(cx, ConsoleLogLevel::Dir, vec![argument.clone()], None),
880 );
881 Self::send_to_embedder(
882 global,
883 ConsoleLogLevel::Dir,
884 format!("{prefix}{}", stringify_debugger_value(&argument)),
885 );
886 }
887
888 fn Assert(
890 cx: &mut JSContext,
891 global: &GlobalScope,
892 condition: bool,
893 messages: Vec<HandleValue>,
894 ) {
895 if !condition {
896 let message = format!(
897 "Assertion failed: {}",
898 stringify_handle_values(cx, &messages)
899 );
900
901 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
902 }
903 }
904
905 fn Time(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
907 if let Ok(()) = global.time(label.clone()) {
908 let message = format!("{label}: timer started");
909 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
910 }
911 }
912
913 fn TimeLog(cx: &mut JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
915 if let Ok(delta) = global.time_log(&label) {
916 let message = format!("{label}: {delta}ms {}", stringify_handle_values(cx, &data));
917
918 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
919 }
920 }
921
922 fn TimeEnd(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
924 if let Ok(delta) = global.time_end(&label) {
925 let message = format!("{label}: {delta}ms");
926
927 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
928 }
929 }
930
931 fn Group(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
933 global.push_console_group(stringify_handle_values(cx, &messages));
934 }
935
936 fn GroupCollapsed(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
938 global.push_console_group(stringify_handle_values(cx, &messages));
939 }
940
941 fn GroupEnd(global: &GlobalScope) {
943 global.pop_console_group();
944 }
945
946 fn Count(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
948 let count = global.increment_console_count(&label);
949 let message = format!("{label}: {count}");
950
951 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
952 }
953
954 fn CountReset(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
956 if global.reset_console_count(&label).is_err() {
957 Self::internal_warn(cx, global, format!("Counter “{label}” doesn’t exist."))
958 }
959 }
960}
961
962#[expect(unsafe_code)]
963fn get_js_stack(cx: &mut JSContext) -> Vec<StackFrame> {
964 const MAX_FRAME_COUNT: u32 = 128;
965
966 let mut frames = vec![];
967 rooted!(&in(cx) let mut handle = ptr::null_mut());
968 let captured_js_stack =
969 unsafe { CapturedJSStack::new(cx.raw_cx(), handle, Some(MAX_FRAME_COUNT)) };
970 let Some(captured_js_stack) = captured_js_stack else {
971 return frames;
972 };
973
974 captured_js_stack.for_each_stack_frame(|frame| {
975 rooted!(&in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
976
977 unsafe {
979 GetSavedFrameFunctionDisplayName(
980 cx,
981 ptr::null_mut(),
982 frame,
983 result.handle_mut(),
984 SavedFrameSelfHosted::Include,
985 );
986 }
987 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
988 unsafe { jsstr_to_string(cx, nonnull_result) }
989 } else {
990 "<anonymous>".into()
991 };
992
993 result.set(ptr::null_mut());
995 unsafe {
996 GetSavedFrameSource(
997 cx,
998 ptr::null_mut(),
999 frame,
1000 result.handle_mut(),
1001 SavedFrameSelfHosted::Include,
1002 );
1003 }
1004 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1005 unsafe { jsstr_to_string(cx, nonnull_result) }
1006 } else {
1007 "<anonymous>".into()
1008 };
1009
1010 let mut line_number = 0;
1012 unsafe {
1013 GetSavedFrameLine(
1014 cx,
1015 ptr::null_mut(),
1016 frame,
1017 &mut line_number,
1018 SavedFrameSelfHosted::Include,
1019 );
1020 }
1021
1022 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
1023 unsafe {
1024 GetSavedFrameColumn(
1025 cx,
1026 ptr::null_mut(),
1027 frame,
1028 &mut column_number,
1029 SavedFrameSelfHosted::Include,
1030 );
1031 }
1032 let frame = StackFrame {
1033 filename,
1034 function_name,
1035 line_number,
1036 column_number: column_number.value_,
1037 };
1038
1039 frames.push(frame);
1040 });
1041
1042 frames
1043}