feat(minifier): drop console statements and exprs (#552)

This commit is contained in:
Don Isaac 2023-07-16 02:41:51 -04:00 committed by GitHub
parent 707e717f0d
commit 2591194dba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 83 additions and 13 deletions

View file

@ -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);

View file

@ -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"
}

View file

@ -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}");
}

View file

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