//! This module implements the global `console` object.
//!
//! The `console` object can be accessed from any global object.
//!
//! The specifics of how it works varies from browser to browser, but there is a de facto set of features that are typically provided.
//!
//! More information:
//! - [MDN documentation][mdn]
//! - [WHATWG `console` specification][spec]
//!
//! [spec]: https://console.spec.whatwg.org/
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Console
#![allow(clippy::print_stdout)]
#[cfg(test)]
mod tests;
use crate::{
builtins::{BuiltIn, JsArgs},
object::ObjectInitializer,
value::{display::display_obj, JsValue, Numeric},
Context, JsResult, JsString,
};
use boa_profiler::Profiler;
use rustc_hash::FxHashMap;
use std::time::SystemTime;
use tap::{Conv, Pipe};
/// This represents the different types of log messages.
#[derive(Debug)]
pub enum LogMessage {
Log(String),
Info(String),
Warn(String),
Error(String),
}
/// Helper function for logging messages.
pub(crate) fn logger(msg: LogMessage, console_state: &Console) {
let indent = 2 * console_state.groups.len();
match msg {
LogMessage::Error(msg) => {
eprintln!("{msg:>indent$}");
}
LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => {
println!("{msg:>indent$}");
}
}
}
/// This represents the `console` formatter.
pub fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
let target = data
.get(0)
.cloned()
.unwrap_or_default()
.to_string(context)?;
match data.len() {
0 => Ok(String::new()),
1 => Ok(target.to_string()),
_ => {
let mut formatted = String::new();
let mut arg_index = 1;
let mut chars = target.chars();
while let Some(c) = chars.next() {
if c == '%' {
let fmt = chars.next().unwrap_or('%');
match fmt {
/* integer */
'd' | 'i' => {
let arg = match data.get_or_undefined(arg_index).to_numeric(context)? {
Numeric::Number(r) => (r.floor() + 0.0).to_string(),
Numeric::BigInt(int) => int.to_string(),
};
formatted.push_str(&arg);
arg_index += 1;
}
/* float */
'f' => {
let arg = data.get_or_undefined(arg_index).to_number(context)?;
formatted.push_str(&format!("{arg:.6}"));
arg_index += 1;
}
/* object, FIXME: how to render this properly? */
'o' | 'O' => {
let arg = data.get_or_undefined(arg_index);
formatted.push_str(&arg.display().to_string());
arg_index += 1;
}
/* string */
's' => {
let arg = data
.get(arg_index)
.cloned()
.unwrap_or_default()
.to_string(context)?;
formatted.push_str(&arg);
arg_index += 1;
}
'%' => formatted.push('%'),
/* TODO: %c is not implemented */
c => {
formatted.push('%');
formatted.push(c);
}
}
} else {
formatted.push(c);
};
}
/* unformatted data */
for rest in data.iter().skip(arg_index) {
formatted.push_str(&format!(" {}", rest.to_string(context)?));
}
Ok(formatted)
}
}
}
/// This is the internal console object state.
#[derive(Debug, Default)]
pub(crate) struct Console {
count_map: FxHashMap<JsString, u32>,
timer_map: FxHashMap<JsString, u128>,
groups: Vec<String>,
}
impl BuiltIn for Console {
const NAME: &'static str = "console";
fn init(context: &mut Context) -> Option<JsValue> {
let _timer = Profiler::global().start_event(Self::NAME, "init");
ObjectInitializer::new(context)
.function(Self::assert, "assert", 0)
.function(Self::clear, "clear", 0)
.function(Self::debug, "debug", 0)
.function(Self::error, "error", 0)
.function(Self::info, "info", 0)
.function(Self::log, "log", 0)
.function(Self::trace, "trace", 0)
.function(Self::warn, "warn", 0)
.function(Self::error, "exception", 0)
.function(Self::count, "count", 0)
.function(Self::count_reset, "countReset", 0)
.function(Self::group, "group", 0)
.function(Self::group, "groupCollapsed", 0)
.function(Self::group_end, "groupEnd", 0)
.function(Self::time, "time", 0)
.function(Self::time_log, "timeLog", 0)
.function(Self::time_end, "timeEnd", 0)
.function(Self::dir, "dir", 0)
.function(Self::dir, "dirxml", 0)
.build()
.conv::<JsValue>()
.pipe(Some)
}
}
impl Console {
/// The name of the object.
pub(crate) const NAME: &'static str = "console";
/// `console.assert(condition, ...data)`
///
/// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there
/// were no arguments.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#assert
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert
pub(crate) fn assert(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let assertion = args.get(0).map_or(false, JsValue::to_boolean);
if !assertion {
let mut args: Vec<JsValue> = args.iter().skip(1).cloned().collect();
let message = "Assertion failed".to_string();
if args.is_empty() {
args.push(JsValue::new(message));
} else if !args[0].is_string() {
args.insert(0, JsValue::new(message));
} else {
let concat = format!("{message}: {}", args[0].display());
args[0] = JsValue::new(concat);
}
logger(
LogMessage::Error(formatter(&args, context)?),
context.console(),
);
}
Ok(JsValue::undefined())
}
/// `console.clear()`
///
/// Removes all groups and clears console if possible.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#clear
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn clear(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
context.console_mut().groups.clear();
Ok(JsValue::undefined())
}
/// `console.debug(...data)`
///
/// Prints a JavaScript values with "debug" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#debug
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug
pub(crate) fn debug(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Log(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.error(...data)`
///
/// Prints a JavaScript values with "error" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#error
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error
pub(crate) fn error(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Error(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.info(...data)`
///
/// Prints a JavaScript values with "info" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#info
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info
pub(crate) fn info(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Info(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.log(...data)`
///
/// Prints a JavaScript values with "log" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#log
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log
pub(crate) fn log(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Log(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
fn get_stack_trace(context: &mut Context) -> Vec<String> {
let mut stack_trace: Vec<String> = vec![];
for frame in context.vm.frames.iter().rev() {
stack_trace.push(
context
.interner()
.resolve_expect(frame.code.name)
.to_owned(),
);
}
stack_trace
}
/// `console.trace(...data)`
///
/// Prints a stack trace with "trace" logLevel, optionally labelled by data.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#trace
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace
pub(crate) fn trace(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if !args.is_empty() {
logger(
LogMessage::Log(formatter(args, context)?),
context.console(),
);
let stack_trace_dump = Self::get_stack_trace(context).join("\n");
logger(LogMessage::Log(stack_trace_dump), context.console());
}
Ok(JsValue::undefined())
}
/// `console.warn(...data)`
///
/// Prints a JavaScript values with "warn" logLevel.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#warn
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn
pub(crate) fn warn(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Warn(formatter(args, context)?),
context.console(),
);
Ok(JsValue::undefined())
}
/// `console.count(label)`
///
/// Prints number of times the function was called with that particular label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#count
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count
pub(crate) fn count(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
let msg = format!("count {label}:");
let c = context.console_mut().count_map.entry(label).or_insert(0);
*c += 1;
logger(LogMessage::Info(format!("{msg} {c}")), context.console());
Ok(JsValue::undefined())
}
/// `console.countReset(label)`
///
/// Resets the counter for label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#countreset
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset
pub(crate) fn count_reset(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
context.console_mut().count_map.remove(&label);
logger(
LogMessage::Warn(format!("countReset {label}")),
context.console(),
);
Ok(JsValue::undefined())
}
/// Returns current system time in ms.
fn system_time_in_ms() -> u128 {
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH)
.expect("negative duration")
.as_millis()
}
/// `console.time(label)`
///
/// Starts the timer for given label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#time
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time
pub(crate) fn time(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if context.console().timer_map.get(&label).is_some() {
logger(
LogMessage::Warn(format!("Timer '{label}' already exist")),
context.console(),
);
} else {
let time = Self::system_time_in_ms();
context.console_mut().timer_map.insert(label, time);
}
Ok(JsValue::undefined())
}
/// `console.timeLog(label, ...data)`
///
/// Prints elapsed time for timer with given label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#timelog
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog
pub(crate) fn time_log(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Some(t) = context.console().timer_map.get(&label) {
let time = Self::system_time_in_ms();
let mut concat = format!("{label}: {} ms", time - t);
for msg in args.iter().skip(1) {
concat = concat + " " + &msg.display().to_string();
}
logger(LogMessage::Log(concat), context.console());
} else {
logger(
LogMessage::Warn(format!("Timer '{label}' doesn't exist")),
context.console(),
);
}
Ok(JsValue::undefined())
}
/// `console.timeEnd(label)`
///
/// Removes the timer with given label.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#timeend
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd
pub(crate) fn time_end(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let label = match args.get(0) {
Some(value) => value.to_string(context)?,
None => "default".into(),
};
if let Some(t) = context.console_mut().timer_map.remove(label.as_str()) {
let time = Self::system_time_in_ms();
logger(
LogMessage::Info(format!("{label}: {} ms - timer removed", time - t)),
context.console(),
);
} else {
logger(
LogMessage::Warn(format!("Timer '{label}' doesn't exist")),
context.console(),
);
}
Ok(JsValue::undefined())
}
/// `console.group(...data)`
///
/// Adds new group with name from formatted data to stack.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#group
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group
pub(crate) fn group(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let group_label = formatter(args, context)?;
logger(
LogMessage::Info(format!("group: {group_label}")),
context.console(),
);
context.console_mut().groups.push(group_label);
Ok(JsValue::undefined())
}
/// `console.groupEnd(label)`
///
/// Removes the last group from the stack.
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#groupend
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn group_end(
_: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
context.console_mut().groups.pop();
Ok(JsValue::undefined())
}
/// `console.dir(item, options)`
///
/// Prints info about item
///
/// More information:
/// - [MDN documentation][mdn]
/// - [WHATWG `console` specification][spec]
///
/// [spec]: https://console.spec.whatwg.org/#dir
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn dir(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
logger(
LogMessage::Info(display_obj(args.get_or_undefined(0), true)),
context.console(),
);
Ok(JsValue::undefined())
}
}