diff --git a/crates/compiler/src/evaluate/visitor.rs b/crates/compiler/src/evaluate/visitor.rs index 5e346fa0..d022999f 100644 --- a/crates/compiler/src/evaluate/visitor.rs +++ b/crates/compiler/src/evaluate/visitor.rs @@ -1042,14 +1042,10 @@ impl<'a> Visitor<'a> { } let message = self.visit_expr(debug_rule.value)?; + let message = message.inspect(debug_rule.span)?; let loc = self.map.look_up_span(debug_rule.span); - eprintln!( - "{}:{} DEBUG: {}", - loc.file.name(), - loc.begin.line + 1, - message.inspect(debug_rule.span)? - ); + self.options.logger.debug(loc, message.as_str()); Ok(None) } @@ -1588,13 +1584,7 @@ impl<'a> Visitor<'a> { return; } let loc = self.map.look_up_span(span); - eprintln!( - "Warning: {}\n ./{}:{}:{}", - message, - loc.file.name(), - loc.begin.line + 1, - loc.begin.column + 1 - ); + self.options.logger.warning(loc, message); } fn visit_warn_rule(&mut self, warn_rule: AstWarn) -> SassResult<()> { diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 796af246..63318f0c 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -80,6 +80,7 @@ pub use crate::error::{ PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result, }; pub use crate::fs::{Fs, NullFs, StdFs}; +pub use crate::logger::{Logger, NullLogger, StdLogger}; pub use crate::options::{InputSyntax, Options, OutputStyle}; pub use crate::{builtin::Builtin, evaluate::Visitor}; pub(crate) use crate::{context_flags::ContextFlags, lexer::Token}; @@ -114,6 +115,7 @@ mod evaluate; mod fs; mod interner; mod lexer; +mod logger; mod options; mod parse; mod selector; diff --git a/crates/compiler/src/logger.rs b/crates/compiler/src/logger.rs new file mode 100644 index 00000000..e31109a8 --- /dev/null +++ b/crates/compiler/src/logger.rs @@ -0,0 +1,50 @@ +use codemap::SpanLoc; +use std::fmt::Debug; + +/// Sink for log messages +pub trait Logger: Debug { + /// Logs message from a `@debug` statement + fn debug(&self, location: SpanLoc, message: &str); + + /// Logs message from a `@warn` statement + fn warning(&self, location: SpanLoc, message: &str); +} + +/// Logs events to standard error +#[derive(Debug)] +pub struct StdLogger; + +impl Logger for StdLogger { + #[inline] + fn debug(&self, location: SpanLoc, message: &str) { + eprintln!( + "{}:{} DEBUG: {}", + location.file.name(), + location.begin.line + 1, + message + ); + } + + #[inline] + fn warning(&self, location: SpanLoc, message: &str) { + eprintln!( + "Warning: {}\n ./{}:{}:{}", + message, + location.file.name(), + location.begin.line + 1, + location.begin.column + 1 + ); + } +} + +/// Discards all log events +#[derive(Debug)] +pub struct NullLogger; + +impl Logger for NullLogger { + #[inline] + fn debug(&self, _location: SpanLoc, _message: &str) {} + + #[inline] + fn warning(&self, _location: SpanLoc, _message: &str) {} +} diff --git a/crates/compiler/src/options.rs b/crates/compiler/src/options.rs index b0528a68..44e896f9 100644 --- a/crates/compiler/src/options.rs +++ b/crates/compiler/src/options.rs @@ -3,7 +3,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{builtin::Builtin, Fs, StdFs}; +use crate::{builtin::Builtin, Fs, Logger, StdFs, StdLogger}; /// Configuration for Sass compilation /// @@ -12,10 +12,12 @@ use crate::{builtin::Builtin, Fs, StdFs}; #[derive(Debug)] pub struct Options<'a> { pub(crate) fs: &'a dyn Fs, + pub(crate) logger: &'a dyn Logger, pub(crate) style: OutputStyle, pub(crate) load_paths: Vec, pub(crate) allows_charset: bool, pub(crate) unicode_error_messages: bool, + // TODO: remove in favor of NullLogger pub(crate) quiet: bool, pub(crate) input_syntax: Option, pub(crate) custom_fns: HashMap, @@ -26,6 +28,7 @@ impl Default for Options<'_> { fn default() -> Self { Self { fs: &StdFs, + logger: &StdLogger, style: OutputStyle::Expanded, load_paths: Vec::new(), allows_charset: true, @@ -49,6 +52,16 @@ impl<'a> Options<'a> { self } + /// This option allows you to define how log events should be handled + /// + /// Be default, [`StdLogger`] is used, which writes all events to standard output. + #[must_use] + #[inline] + pub fn logger(mut self, logger: &'a dyn Logger) -> Self { + self.logger = logger; + self + } + /// `grass` currently offers 2 different output styles /// /// - [`OutputStyle::Expanded`] writes each selector and declaration on its own line. @@ -67,10 +80,12 @@ impl<'a> Options<'a> { /// when compiling. By default, Sass emits warnings /// when deprecated features are used or when the /// `@warn` rule is encountered. It also silences the - /// `@debug` rule. + /// `@debug` rule. Setting this option to `true` will + /// stop all events from reaching the assigned [`logger`]. /// /// By default, this value is `false` and warnings are emitted. #[must_use] + #[deprecated = "use `logger(&NullLogger)` instead"] #[inline] pub const fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 8ae8c863..49daa45b 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -66,8 +66,8 @@ grass input.scss )] pub use grass_compiler::{ - from_path, from_string, Error, ErrorKind, Fs, InputSyntax, NullFs, Options, OutputStyle, - Result, StdFs, + from_path, from_string, Error, ErrorKind, Fs, InputSyntax, Logger, NullFs, NullLogger, Options, + OutputStyle, Result, StdFs, StdLogger, }; /// Include CSS in your binary at compile time from a Sass source file diff --git a/crates/lib/tests/debug.rs b/crates/lib/tests/debug.rs index 545667ad..5e0b1c3c 100644 --- a/crates/lib/tests/debug.rs +++ b/crates/lib/tests/debug.rs @@ -1,12 +1,37 @@ +use macros::TestLogger; + #[macro_use] mod macros; -test!(simple_debug, "@debug 2", ""); -test!(simple_debug_with_semicolon, "@debug 2;", ""); -test!( - // todo: test stdout - debug_while_quiet, - "@debug 2;", - "", - grass::Options::default().quiet(true) -); +#[test] +fn simple_debug() { + let input = "@debug 2"; + let logger = TestLogger::default(); + let options = grass::Options::default().logger(&logger); + let output = grass::from_string(input.to_string(), &options).expect(input); + assert_eq!(&output, ""); + assert_eq!(&[String::from("2")], logger.debug_messages().as_slice()); + assert_eq!(&[] as &[String], logger.warning_messages().as_slice()); +} + +#[test] +fn simple_debug_with_semicolon() { + let input = "@debug 2;"; + let logger = TestLogger::default(); + let options = grass::Options::default().logger(&logger); + let output = grass::from_string(input.to_string(), &options).expect(input); + assert_eq!(&output, ""); + assert_eq!(&[String::from("2")], logger.debug_messages().as_slice()); + assert_eq!(&[] as &[String], logger.warning_messages().as_slice()); +} + +#[test] +fn debug_while_quiet() { + let input = "@debug 2;"; + let logger = TestLogger::default(); + let options = grass::Options::default().logger(&logger).quiet(true); + let output = grass::from_string(input.to_string(), &options).expect(input); + assert_eq!(&output, ""); + assert_eq!(&[] as &[String], logger.debug_messages().as_slice()); + assert_eq!(&[] as &[String], logger.warning_messages().as_slice()); +} diff --git a/crates/lib/tests/macros.rs b/crates/lib/tests/macros.rs index 105227a2..7d776909 100644 --- a/crates/lib/tests/macros.rs +++ b/crates/lib/tests/macros.rs @@ -1,10 +1,12 @@ use std::{ borrow::Cow, + cell::RefCell, collections::BTreeMap, path::{Path, PathBuf}, }; -use grass::Fs; +use grass::{Fs, Logger}; +use grass_compiler::codemap::SpanLoc; #[macro_export] macro_rules! test { @@ -162,3 +164,32 @@ impl Fs for TestFs { Ok(self.files.get(path).unwrap().as_bytes().to_vec()) } } + +#[derive(Debug, Default)] +struct TestLoggerState { + debug_messages: Vec, + warning_messages: Vec, +} + +#[derive(Debug, Default)] +pub struct TestLogger(RefCell); + +impl TestLogger { + pub fn debug_messages(&self) -> Vec { + self.0.borrow().debug_messages.clone() + } + + pub fn warning_messages(&self) -> Vec { + self.0.borrow().warning_messages.clone() + } +} + +impl Logger for TestLogger { + fn debug(&self, _location: SpanLoc, message: &str) { + self.0.borrow_mut().debug_messages.push(message.into()); + } + + fn warning(&self, _location: SpanLoc, message: &str) { + self.0.borrow_mut().warning_messages.push(message.into()); + } +} diff --git a/crates/lib/tests/warn.rs b/crates/lib/tests/warn.rs index 41b42542..65c78904 100644 --- a/crates/lib/tests/warn.rs +++ b/crates/lib/tests/warn.rs @@ -1,11 +1,37 @@ +use macros::TestLogger; + #[macro_use] mod macros; -test!(simple_warn, "@warn 2", ""); -test!( - // todo: test stdout - warn_while_quiet, - "@warn 2;", - "", - grass::Options::default().quiet(true) -); +#[test] +fn warn_debug() { + let input = "@warn 2"; + let logger = TestLogger::default(); + let options = grass::Options::default().logger(&logger); + let output = grass::from_string(input.to_string(), &options).expect(input); + assert_eq!(&output, ""); + assert_eq!(&[] as &[String], logger.debug_messages().as_slice()); + assert_eq!(&[String::from("2")], logger.warning_messages().as_slice()); +} + +#[test] +fn simple_warn_with_semicolon() { + let input = "@warn 2;"; + let logger = TestLogger::default(); + let options = grass::Options::default().logger(&logger); + let output = grass::from_string(input.to_string(), &options).expect(input); + assert_eq!(&output, ""); + assert_eq!(&[] as &[String], logger.debug_messages().as_slice()); + assert_eq!(&[String::from("2")], logger.warning_messages().as_slice()); +} + +#[test] +fn warn_while_quiet() { + let input = "@warn 2;"; + let logger = TestLogger::default(); + let options = grass::Options::default().logger(&logger).quiet(true); + let output = grass::from_string(input.to_string(), &options).expect(input); + assert_eq!(&output, ""); + assert_eq!(&[] as &[String], logger.debug_messages().as_slice()); + assert_eq!(&[] as &[String], logger.warning_messages().as_slice()); +}