Add logger trait (#93)

Introduced a new `Logger` trait which can be used to access all log events
emitted during compilation. Currently these only include messages emitted by the
`@debug` and `@warn` statements.

Changes were implemented in a backwards-compatible manner, but the current
`Options::quiet` method has been marked as deprecated, as its behavior can be
achieved using the `NullLogger` structure. The default logger used is
`StdLogger` which writes all log events to standard error. This reflect the
default behavior prior to introduction of `Logger`.

With these new changes, it is also now possible to properly test the `@debug`
and `@warn` statements.
This commit is contained in:
Matyáš Pokorný 2024-05-19 05:44:40 +02:00 committed by GitHub
parent 8d3258dcd4
commit a1ca700bff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 174 additions and 35 deletions

View File

@ -1042,14 +1042,10 @@ impl<'a> Visitor<'a> {
} }
let message = self.visit_expr(debug_rule.value)?; 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); let loc = self.map.look_up_span(debug_rule.span);
eprintln!( self.options.logger.debug(loc, message.as_str());
"{}:{} DEBUG: {}",
loc.file.name(),
loc.begin.line + 1,
message.inspect(debug_rule.span)?
);
Ok(None) Ok(None)
} }
@ -1588,13 +1584,7 @@ impl<'a> Visitor<'a> {
return; return;
} }
let loc = self.map.look_up_span(span); let loc = self.map.look_up_span(span);
eprintln!( self.options.logger.warning(loc, message);
"Warning: {}\n ./{}:{}:{}",
message,
loc.file.name(),
loc.begin.line + 1,
loc.begin.column + 1
);
} }
fn visit_warn_rule(&mut self, warn_rule: AstWarn) -> SassResult<()> { fn visit_warn_rule(&mut self, warn_rule: AstWarn) -> SassResult<()> {

View File

@ -80,6 +80,7 @@ pub use crate::error::{
PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result, PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result,
}; };
pub use crate::fs::{Fs, NullFs, StdFs}; pub use crate::fs::{Fs, NullFs, StdFs};
pub use crate::logger::{Logger, NullLogger, StdLogger};
pub use crate::options::{InputSyntax, Options, OutputStyle}; pub use crate::options::{InputSyntax, Options, OutputStyle};
pub use crate::{builtin::Builtin, evaluate::Visitor}; pub use crate::{builtin::Builtin, evaluate::Visitor};
pub(crate) use crate::{context_flags::ContextFlags, lexer::Token}; pub(crate) use crate::{context_flags::ContextFlags, lexer::Token};
@ -114,6 +115,7 @@ mod evaluate;
mod fs; mod fs;
mod interner; mod interner;
mod lexer; mod lexer;
mod logger;
mod options; mod options;
mod parse; mod parse;
mod selector; mod selector;

View File

@ -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) {}
}

View File

@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::{builtin::Builtin, Fs, StdFs}; use crate::{builtin::Builtin, Fs, Logger, StdFs, StdLogger};
/// Configuration for Sass compilation /// Configuration for Sass compilation
/// ///
@ -12,10 +12,12 @@ use crate::{builtin::Builtin, Fs, StdFs};
#[derive(Debug)] #[derive(Debug)]
pub struct Options<'a> { pub struct Options<'a> {
pub(crate) fs: &'a dyn Fs, pub(crate) fs: &'a dyn Fs,
pub(crate) logger: &'a dyn Logger,
pub(crate) style: OutputStyle, pub(crate) style: OutputStyle,
pub(crate) load_paths: Vec<PathBuf>, pub(crate) load_paths: Vec<PathBuf>,
pub(crate) allows_charset: bool, pub(crate) allows_charset: bool,
pub(crate) unicode_error_messages: bool, pub(crate) unicode_error_messages: bool,
// TODO: remove in favor of NullLogger
pub(crate) quiet: bool, pub(crate) quiet: bool,
pub(crate) input_syntax: Option<InputSyntax>, pub(crate) input_syntax: Option<InputSyntax>,
pub(crate) custom_fns: HashMap<String, Builtin>, pub(crate) custom_fns: HashMap<String, Builtin>,
@ -26,6 +28,7 @@ impl Default for Options<'_> {
fn default() -> Self { fn default() -> Self {
Self { Self {
fs: &StdFs, fs: &StdFs,
logger: &StdLogger,
style: OutputStyle::Expanded, style: OutputStyle::Expanded,
load_paths: Vec::new(), load_paths: Vec::new(),
allows_charset: true, allows_charset: true,
@ -49,6 +52,16 @@ impl<'a> Options<'a> {
self 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 /// `grass` currently offers 2 different output styles
/// ///
/// - [`OutputStyle::Expanded`] writes each selector and declaration on its own line. /// - [`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 compiling. By default, Sass emits warnings
/// when deprecated features are used or when the /// when deprecated features are used or when the
/// `@warn` rule is encountered. It also silences 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. /// By default, this value is `false` and warnings are emitted.
#[must_use] #[must_use]
#[deprecated = "use `logger(&NullLogger)` instead"]
#[inline] #[inline]
pub const fn quiet(mut self, quiet: bool) -> Self { pub const fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet; self.quiet = quiet;

View File

@ -66,8 +66,8 @@ grass input.scss
)] )]
pub use grass_compiler::{ pub use grass_compiler::{
from_path, from_string, Error, ErrorKind, Fs, InputSyntax, NullFs, Options, OutputStyle, from_path, from_string, Error, ErrorKind, Fs, InputSyntax, Logger, NullFs, NullLogger, Options,
Result, StdFs, OutputStyle, Result, StdFs, StdLogger,
}; };
/// Include CSS in your binary at compile time from a Sass source file /// Include CSS in your binary at compile time from a Sass source file

View File

@ -1,12 +1,37 @@
use macros::TestLogger;
#[macro_use] #[macro_use]
mod macros; mod macros;
test!(simple_debug, "@debug 2", ""); #[test]
test!(simple_debug_with_semicolon, "@debug 2;", ""); fn simple_debug() {
test!( let input = "@debug 2";
// todo: test stdout let logger = TestLogger::default();
debug_while_quiet, let options = grass::Options::default().logger(&logger);
"@debug 2;", let output = grass::from_string(input.to_string(), &options).expect(input);
"", assert_eq!(&output, "");
grass::Options::default().quiet(true) 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());
}

View File

@ -1,10 +1,12 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell,
collections::BTreeMap, collections::BTreeMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use grass::Fs; use grass::{Fs, Logger};
use grass_compiler::codemap::SpanLoc;
#[macro_export] #[macro_export]
macro_rules! test { macro_rules! test {
@ -162,3 +164,32 @@ impl Fs for TestFs {
Ok(self.files.get(path).unwrap().as_bytes().to_vec()) Ok(self.files.get(path).unwrap().as_bytes().to_vec())
} }
} }
#[derive(Debug, Default)]
struct TestLoggerState {
debug_messages: Vec<String>,
warning_messages: Vec<String>,
}
#[derive(Debug, Default)]
pub struct TestLogger(RefCell<TestLoggerState>);
impl TestLogger {
pub fn debug_messages(&self) -> Vec<String> {
self.0.borrow().debug_messages.clone()
}
pub fn warning_messages(&self) -> Vec<String> {
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());
}
}

View File

@ -1,11 +1,37 @@
use macros::TestLogger;
#[macro_use] #[macro_use]
mod macros; mod macros;
test!(simple_warn, "@warn 2", ""); #[test]
test!( fn warn_debug() {
// todo: test stdout let input = "@warn 2";
warn_while_quiet, let logger = TestLogger::default();
"@warn 2;", let options = grass::Options::default().logger(&logger);
"", let output = grass::from_string(input.to_string(), &options).expect(input);
grass::Options::default().quiet(true) 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());
}