From c6c6e4c1ae9c88045b53ed4b0914a887182bdb1e Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 14 Aug 2024 09:20:10 -0700 Subject: [PATCH] Allow a custom Logger to be used as the backend for boa_runtime::Console (#3943) * Allow a custom Logger to be used as the backend for boa_runtime::Console * Comments --- core/runtime/src/console/mod.rs | 257 +++++++++++++++++++++----------- core/runtime/src/lib.rs | 2 +- 2 files changed, 167 insertions(+), 92 deletions(-) diff --git a/core/runtime/src/console/mod.rs b/core/runtime/src/console/mod.rs index 8953e32228..b4507e3e48 100644 --- a/core/runtime/src/console/mod.rs +++ b/core/runtime/src/console/mod.rs @@ -25,26 +25,64 @@ use boa_gc::{Finalize, Trace}; use rustc_hash::FxHashMap; use std::{cell::RefCell, collections::hash_map::Entry, rc::Rc, time::SystemTime}; -/// This represents the different types of log messages. -#[derive(Debug)] -enum LogMessage { - Log(String), - Info(String), - Warn(String), - Error(String), +/// A trait that can be used to forward console logs to an implementation. +pub trait Logger: Trace + Sized { + /// Log a log message (`console.log`). + /// + /// # Errors + /// Returning an error will throw an exception in JavaScript. + fn log(&self, msg: String, state: &Console) -> JsResult<()>; + + /// Log an info message (`console.info`). + /// + /// # Errors + /// Returning an error will throw an exception in JavaScript. + fn info(&self, msg: String, state: &Console) -> JsResult<()>; + + /// Log a warning message (`console.warn`). + /// + /// # Errors + /// Returning an error will throw an exception in JavaScript. + fn warn(&self, msg: String, state: &Console) -> JsResult<()>; + + /// Log an error message (`console.error`). + /// + /// # Errors + /// Returning an error will throw an exception in JavaScript. + fn error(&self, msg: String, state: &Console) -> JsResult<()>; } -/// Helper function for logging messages. -fn logger(msg: LogMessage, console_state: &Console) { - let indent = 2 * console_state.groups.len(); +/// The default implementation for logging from the console. +/// +/// Implements the [`Logger`] trait and output errors to stderr and all +/// the others to stdout. Will add indentation based on the number of +/// groups. +#[derive(Trace, Finalize)] +struct DefaultLogger; + +impl Logger for DefaultLogger { + #[inline] + fn log(&self, msg: String, state: &Console) -> JsResult<()> { + let indent = 2 * state.groups.len(); + println!("{msg:>indent$}"); + Ok(()) + } - match msg { - LogMessage::Error(msg) => { - eprintln!("{msg:>indent$}"); - } - LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => { - println!("{msg:>indent$}"); - } + #[inline] + fn info(&self, msg: String, state: &Console) -> JsResult<()> { + self.log(msg, state) + } + + #[inline] + fn warn(&self, msg: String, state: &Console) -> JsResult<()> { + self.log(msg, state) + } + + #[inline] + fn error(&self, msg: String, state: &Console) -> JsResult<()> { + let indent = 2 * state.groups.len(); + eprintln!("{msg:>indent$}"); + Ok(()) } } @@ -123,134 +161,156 @@ fn formatter(data: &[JsValue], context: &mut Context) -> JsResult { /// This is the internal console object state. #[derive(Debug, Default, Trace, Finalize, JsData)] pub struct Console { - count_map: FxHashMap, - timer_map: FxHashMap, - groups: Vec, + /// The map of console counters, used in `console.count()`. + pub count_map: FxHashMap, + + /// The map of console timers, used in `console.time`, `console.timeLog` + /// and `console.timeEnd`. + pub timer_map: FxHashMap, + + /// The current list of groups. Groups should be indented, but some logging + /// libraries may want to use them in a different way. + pub groups: Vec, } impl Console { /// Name of the built-in `console` property. pub const NAME: JsStr<'static> = js_str!("console"); - /// Initializes the `console` built-in object. + /// Initializes the `console` with a special logger. #[allow(clippy::too_many_lines)] - pub fn init(context: &mut Context) -> JsObject { - fn console_method( - f: fn(&JsValue, &[JsValue], &Console, &mut Context) -> JsResult, + pub fn init_with_logger(context: &mut Context, logger: L) -> JsObject + where + L: Logger + 'static, + { + fn console_method( + f: fn(&JsValue, &[JsValue], &Console, &L, &mut Context) -> JsResult, state: Rc>, + logger: Rc, ) -> NativeFunction { // SAFETY: `Console` doesn't contain types that need tracing. unsafe { NativeFunction::from_closure(move |this, args, context| { - f(this, args, &state.borrow(), context) + f(this, args, &state.borrow(), &logger, context) }) } } - fn console_method_mut( - f: fn(&JsValue, &[JsValue], &mut Console, &mut Context) -> JsResult, + fn console_method_mut( + f: fn(&JsValue, &[JsValue], &mut Console, &L, &mut Context) -> JsResult, state: Rc>, + logger: Rc, ) -> NativeFunction { // SAFETY: `Console` doesn't contain types that need tracing. unsafe { NativeFunction::from_closure(move |this, args, context| { - f(this, args, &mut state.borrow_mut(), context) + f(this, args, &mut state.borrow_mut(), &logger, context) }) } } // let _timer = Profiler::global().start_event(std::any::type_name::(), "init"); let state = Rc::new(RefCell::new(Self::default())); + let logger = Rc::new(logger); ObjectInitializer::with_native_data(Self::default(), context) .function( - console_method(Self::assert, state.clone()), + console_method(Self::assert, state.clone(), logger.clone()), js_string!("assert"), 0, ) .function( - console_method_mut(Self::clear, state.clone()), + console_method_mut(Self::clear, state.clone(), logger.clone()), js_string!("clear"), 0, ) .function( - console_method(Self::debug, state.clone()), + console_method(Self::debug, state.clone(), logger.clone()), js_string!("debug"), 0, ) .function( - console_method(Self::error, state.clone()), + console_method(Self::error, state.clone(), logger.clone()), js_string!("error"), 0, ) .function( - console_method(Self::info, state.clone()), + console_method(Self::info, state.clone(), logger.clone()), js_string!("info"), 0, ) .function( - console_method(Self::log, state.clone()), + console_method(Self::log, state.clone(), logger.clone()), js_string!("log"), 0, ) .function( - console_method(Self::trace, state.clone()), + console_method(Self::trace, state.clone(), logger.clone()), js_string!("trace"), 0, ) .function( - console_method(Self::warn, state.clone()), + console_method(Self::warn, state.clone(), logger.clone()), js_string!("warn"), 0, ) .function( - console_method_mut(Self::count, state.clone()), + console_method_mut(Self::count, state.clone(), logger.clone()), js_string!("count"), 0, ) .function( - console_method_mut(Self::count_reset, state.clone()), + console_method_mut(Self::count_reset, state.clone(), logger.clone()), js_string!("countReset"), 0, ) .function( - console_method_mut(Self::group, state.clone()), + console_method_mut(Self::group, state.clone(), logger.clone()), js_string!("group"), 0, ) .function( - console_method_mut(Self::group_collapsed, state.clone()), + console_method_mut(Self::group_collapsed, state.clone(), logger.clone()), js_string!("groupCollapsed"), 0, ) .function( - console_method_mut(Self::group_end, state.clone()), + console_method_mut(Self::group_end, state.clone(), logger.clone()), js_string!("groupEnd"), 0, ) .function( - console_method_mut(Self::time, state.clone()), + console_method_mut(Self::time, state.clone(), logger.clone()), js_string!("time"), 0, ) .function( - console_method(Self::time_log, state.clone()), + console_method(Self::time_log, state.clone(), logger.clone()), js_string!("timeLog"), 0, ) .function( - console_method_mut(Self::time_end, state.clone()), + console_method_mut(Self::time_end, state.clone(), logger.clone()), js_string!("timeEnd"), 0, ) .function( - console_method(Self::dir, state.clone()), + console_method(Self::dir, state.clone(), logger.clone()), js_string!("dir"), 0, ) - .function(console_method(Self::dir, state), js_string!("dirxml"), 0) + .function( + console_method(Self::dir, state, logger.clone()), + js_string!("dirxml"), + 0, + ) .build() } + /// Initializes the `console` built-in object. + pub fn init(context: &mut Context) -> JsObject { + Self::init_with_logger(context, DefaultLogger) + } + /// `console.assert(condition, ...data)` /// /// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there @@ -266,6 +326,7 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let assertion = args.first().map_or(false, JsValue::to_boolean); @@ -283,7 +344,7 @@ impl Console { args[0] = JsValue::new(concat); } - logger(LogMessage::Error(formatter(&args, context)?), console); + logger.error(formatter(&args, context)?, console)?; } Ok(JsValue::undefined()) @@ -300,7 +361,13 @@ impl Console { /// [spec]: https://console.spec.whatwg.org/#clear /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear #[allow(clippy::unnecessary_wraps)] - fn clear(_: &JsValue, _: &[JsValue], console: &mut Self, _: &mut Context) -> JsResult { + fn clear( + _: &JsValue, + _: &[JsValue], + console: &mut Self, + _: &impl Logger, + _: &mut Context, + ) -> JsResult { console.groups.clear(); Ok(JsValue::undefined()) } @@ -319,9 +386,10 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { - logger(LogMessage::Log(formatter(args, context)?), console); + logger.log(formatter(args, context)?, console)?; Ok(JsValue::undefined()) } @@ -339,9 +407,10 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { - logger(LogMessage::Error(formatter(args, context)?), console); + logger.error(formatter(args, context)?, console)?; Ok(JsValue::undefined()) } @@ -359,9 +428,10 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { - logger(LogMessage::Info(formatter(args, context)?), console); + logger.info(formatter(args, context)?, console)?; Ok(JsValue::undefined()) } @@ -379,9 +449,10 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { - logger(LogMessage::Log(formatter(args, context)?), console); + logger.log(formatter(args, context)?, console)?; Ok(JsValue::undefined()) } @@ -399,10 +470,11 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { if !args.is_empty() { - logger(LogMessage::Log(formatter(args, context)?), console); + logger.log(formatter(args, context)?, console)?; } let stack_trace_dump = context @@ -413,7 +485,7 @@ impl Console { .map(JsString::to_std_string_escaped) .collect::>() .join("\n"); - logger(LogMessage::Log(stack_trace_dump), console); + logger.log(stack_trace_dump, console)?; Ok(JsValue::undefined()) } @@ -432,9 +504,10 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { - logger(LogMessage::Warn(formatter(args, context)?), console); + logger.warn(formatter(args, context)?, console)?; Ok(JsValue::undefined()) } @@ -452,6 +525,7 @@ impl Console { _: &JsValue, args: &[JsValue], console: &mut Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let label = match args.first() { @@ -463,7 +537,7 @@ impl Console { let c = console.count_map.entry(label).or_insert(0); *c += 1; - logger(LogMessage::Info(format!("{msg} {c}")), console); + logger.info(format!("{msg} {c}"), console)?; Ok(JsValue::undefined()) } @@ -481,6 +555,7 @@ impl Console { _: &JsValue, args: &[JsValue], console: &mut Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let label = match args.first() { @@ -490,10 +565,10 @@ impl Console { console.count_map.remove(&label); - logger( - LogMessage::Warn(format!("countReset {}", label.to_std_string_escaped())), + logger.warn( + format!("countReset {}", label.to_std_string_escaped()), console, - ); + )?; Ok(JsValue::undefined()) } @@ -520,6 +595,7 @@ impl Console { _: &JsValue, args: &[JsValue], console: &mut Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let label = match args.first() { @@ -531,13 +607,10 @@ impl Console { let time = Self::system_time_in_ms(); e.insert(time); } else { - logger( - LogMessage::Warn(format!( - "Timer '{}' already exist", - label.to_std_string_escaped() - )), + logger.warn( + format!("Timer '{}' already exist", label.to_std_string_escaped()), console, - ); + )?; } Ok(JsValue::undefined()) @@ -557,6 +630,7 @@ impl Console { _: &JsValue, args: &[JsValue], console: &Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let label = match args.first() { @@ -566,13 +640,10 @@ impl Console { console.timer_map.get(&label).map_or_else( || { - logger( - LogMessage::Warn(format!( - "Timer '{}' doesn't exist", - label.to_std_string_escaped() - )), + logger.warn( + format!("Timer '{}' doesn't exist", label.to_std_string_escaped()), console, - ); + ) }, |t| { let time = Self::system_time_in_ms(); @@ -580,9 +651,9 @@ impl Console { for msg in args.iter().skip(1) { concat = concat + " " + &msg.display().to_string(); } - logger(LogMessage::Log(concat), console); + logger.log(concat, console) }, - ); + )?; Ok(JsValue::undefined()) } @@ -601,6 +672,7 @@ impl Console { _: &JsValue, args: &[JsValue], console: &mut Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let label = match args.first() { @@ -610,26 +682,23 @@ impl Console { console.timer_map.remove(&label).map_or_else( || { - logger( - LogMessage::Warn(format!( - "Timer '{}' doesn't exist", - label.to_std_string_escaped() - )), + logger.warn( + format!("Timer '{}' doesn't exist", label.to_std_string_escaped()), console, - ); + ) }, |t| { let time = Self::system_time_in_ms(); - logger( - LogMessage::Info(format!( + logger.info( + format!( "{}: {} ms - timer removed", label.to_std_string_escaped(), time - t - )), + ), console, - ); + ) }, - ); + )?; Ok(JsValue::undefined()) } @@ -648,11 +717,12 @@ impl Console { _: &JsValue, args: &[JsValue], console: &mut Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { let group_label = formatter(args, context)?; - logger(LogMessage::Info(format!("group: {group_label}")), console); + logger.info(format!("group: {group_label}"), console)?; console.groups.push(group_label); Ok(JsValue::undefined()) @@ -672,9 +742,10 @@ impl Console { _: &JsValue, args: &[JsValue], console: &mut Self, + logger: &impl Logger, context: &mut Context, ) -> JsResult { - Console::group(&JsValue::Undefined, args, console, context) + Console::group(&JsValue::Undefined, args, console, logger, context) } /// `console.groupEnd(label)` @@ -692,6 +763,7 @@ impl Console { _: &JsValue, _: &[JsValue], console: &mut Self, + _: &impl Logger, _: &mut Context, ) -> JsResult { console.groups.pop(); @@ -710,11 +782,14 @@ impl Console { /// [spec]: https://console.spec.whatwg.org/#dir /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir #[allow(clippy::unnecessary_wraps)] - fn dir(_: &JsValue, args: &[JsValue], console: &Self, _: &mut Context) -> JsResult { - logger( - LogMessage::Info(args.get_or_undefined(0).display_obj(true)), - console, - ); + fn dir( + _: &JsValue, + args: &[JsValue], + console: &Self, + logger: &impl Logger, + _: &mut Context, + ) -> JsResult { + logger.info(args.get_or_undefined(0).display_obj(true), console)?; Ok(JsValue::undefined()) } } diff --git a/core/runtime/src/lib.rs b/core/runtime/src/lib.rs index f25a0ba6e9..d4e1417720 100644 --- a/core/runtime/src/lib.rs +++ b/core/runtime/src/lib.rs @@ -54,7 +54,7 @@ mod console; #[doc(inline)] -pub use console::Console; +pub use console::{Console, Logger}; #[cfg(test)] pub(crate) mod test {