diff --git a/crates/oxc_minifier/src/compressor/mod.rs b/crates/oxc_minifier/src/compressor/mod.rs index 1cbe6d9cb..8eb5e362a 100644 --- a/crates/oxc_minifier/src/compressor/mod.rs +++ b/crates/oxc_minifier/src/compressor/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::unused_self)] mod fold; +mod util; use oxc_allocator::{Allocator, Vec}; #[allow(clippy::wildcard_imports)] @@ -16,30 +17,47 @@ use oxc_syntax::{ #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Copy)] pub struct CompressOptions { - /// Various optimizations for boolean context, for example `!!a ? b : c` → `a ? b : c` - /// Default true + /// Various optimizations for boolean context, for example `!!a ? b : c` → `a ? b : c`. + /// + /// Default `true` pub booleans: bool, - /// Remove `debugger;` statements - /// Default true + /// Remove `debugger;` statements. + /// + /// Default `true` pub drop_debugger: bool, - /// Join consecutive var statements - /// Default true + /// Remove `console.*` statements. + /// + /// Default `false` + pub drop_console: bool, + + /// Join consecutive var statements. + /// + /// Default `true` pub join_vars: bool, /// Optimizations for do, while and for loops when we can statically determine the condition - /// Default: true + /// + /// Default `true` pub loops: bool, /// Transforms `typeof foo == "undefined" into `foo === void 0` - /// Default true + /// + /// Default `true` pub typeofs: bool, } impl Default for CompressOptions { fn default() -> Self { - Self { booleans: true, drop_debugger: true, join_vars: true, loops: true, typeofs: true } + Self { + booleans: true, + drop_debugger: true, + drop_console: false, + join_vars: true, + loops: true, + typeofs: true, + } } } @@ -101,6 +119,22 @@ impl<'a> Compressor<'a> { matches!(stmt, Statement::DebuggerStatement(_)) && self.options.drop_debugger } + /// Drop `console.*` expressions. + /// Enabled by `compress.drop_console + fn drop_console<'b>(&mut self, stmt: &'b Statement<'a>) -> bool { + self.options.drop_console + && matches!(stmt, Statement::ExpressionStatement(expr) if util::is_console(&expr.expression)) + } + + fn compress_console<'b>(&mut self, expr: &'b mut Expression<'a>) -> bool { + if self.options.drop_console && util::is_console(expr) { + *expr = self.create_void_0(); + true + } else { + false + } + } + /// Join consecutive var statements fn join_vars<'b>(&mut self, stmts: &'b mut Vec<'a, Statement<'a>>) { // Collect all the consecutive ranges that contain joinable vars. @@ -265,7 +299,7 @@ impl<'a> Compressor<'a> { impl<'a, 'b> VisitMut<'a, 'b> for Compressor<'a> { fn visit_statements(&mut self, stmts: &'b mut Vec<'a, Statement<'a>>) { - stmts.retain(|stmt| !self.drop_debugger(stmt)); + stmts.retain(|stmt| !(self.drop_debugger(stmt) || self.drop_console(stmt))); self.join_vars(stmts); @@ -290,13 +324,14 @@ impl<'a, 'b> VisitMut<'a, 'b> for Compressor<'a> { fn visit_variable_declaration(&mut self, decl: &'b mut VariableDeclaration<'a>) { for declarator in decl.declarations.iter_mut() { - self.compress_variable_declarator(declarator); self.visit_variable_declarator(declarator); + self.compress_variable_declarator(declarator); } } fn visit_expression(&mut self, expr: &'b mut Expression<'a>) { self.visit_expression_match(expr); + self.compress_console(expr); self.fold_expression(expr); if !self.compress_undefined(expr) { self.compress_boolean(expr); diff --git a/crates/oxc_minifier/src/compressor/util.rs b/crates/oxc_minifier/src/compressor/util.rs new file mode 100644 index 000000000..22907a460 --- /dev/null +++ b/crates/oxc_minifier/src/compressor/util.rs @@ -0,0 +1,10 @@ +use oxc_hir::hir::Expression; + +pub(super) fn is_console(expr: &Expression<'_>) -> bool { + // let Statement::ExpressionStatement(expr) = stmt else { return false }; + let Expression::CallExpression(call_expr) = &expr else { return false }; + let Expression::MemberExpression(member_expr) = &call_expr.callee else { return false }; + let obj = member_expr.object(); + let Some(ident) = obj.get_identifier_reference() else { return false }; + ident.name == "console" +} diff --git a/crates/oxc_minifier/tests/mod.rs b/crates/oxc_minifier/tests/mod.rs index c59b0297d..aa394cd15 100644 --- a/crates/oxc_minifier/tests/mod.rs +++ b/crates/oxc_minifier/tests/mod.rs @@ -11,8 +11,12 @@ use oxc_minifier::{CompressOptions, Minifier, MinifierOptions, PrinterOptions}; use oxc_span::SourceType; pub(crate) fn test(source_text: &str, expected: &str) { - let source_type = SourceType::default(); let options = MinifierOptions { mangle: false, ..MinifierOptions::default() }; + test_with_options(source_text, expected, options); +} + +pub(crate) fn test_with_options(source_text: &str, expected: &str, options: MinifierOptions) { + let source_type = SourceType::default(); let minified = Minifier::new(source_text, source_type, options).build(); assert_eq!(expected, minified, "for source {source_text}"); } diff --git a/crates/oxc_minifier/tests/oxc/code_removal.rs b/crates/oxc_minifier/tests/oxc/code_removal.rs index 98f5baa4f..2b2dc17dc 100644 --- a/crates/oxc_minifier/tests/oxc/code_removal.rs +++ b/crates/oxc_minifier/tests/oxc/code_removal.rs @@ -1,4 +1,4 @@ -use crate::test; +use crate::{test, test_with_options, CompressOptions, MinifierOptions}; #[test] fn undefined_assignment() { @@ -17,3 +17,24 @@ fn undefined_return() { test("function f(){return void foo();}", "function f(){return void foo()}"); test("function f(){if(a()){return undefined;}}", "function f(){if(a())return}"); } + +#[test] +fn console_removal() { + let options = MinifierOptions { + mangle: false, + compress: CompressOptions { drop_console: true, ..CompressOptions::default() }, + ..MinifierOptions::default() + }; + test_with_options("console.log('hi')", "", options); + test_with_options("let x = console.error('oops')", "let x", options); + test_with_options( + "function f() { return console.warn('problem') }", + "function f(){return}", + options, + ); + + // console isn't removed when drop_console is `false`. This is also the + // default value. + let options = MinifierOptions { mangle: false, ..MinifierOptions::default() }; + test_with_options("console.log('hi')", "console.log('hi')", options); +}