Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/oxc_minifier/src/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl<'a> Compressor<'a> {
scoping: Scoping,
options: CompressOptions,
) {
let state = MinifierState::new(options);
let state = MinifierState::new(program.source_type, options);
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
let normalize_options =
NormalizeOptions { convert_while_to_fors: true, convert_const_to_let: true };
Expand All @@ -52,7 +52,7 @@ impl<'a> Compressor<'a> {
scoping: Scoping,
options: CompressOptions,
) {
let state = MinifierState::new(options);
let state = MinifierState::new(program.source_type, options);
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
let normalize_options =
NormalizeOptions { convert_while_to_fors: false, convert_const_to_let: false };
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_minifier/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ impl<'a> Ctx<'a, '_> {
&self.0.state.options
}

pub fn source_type(&self) -> SourceType {
self.0.state.source_type
}

pub fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool {
ident.is_global_reference(self.0.scoping())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,15 +589,13 @@ mod test {

use crate::{
CompressOptions,
tester::{run, test, test_same},
tester::{test, test_options, test_same},
};

fn test_es2019(source_text: &str, expected: &str) {
let target = ESTarget::ES2019;
assert_eq!(
run(source_text, Some(CompressOptions { target, ..CompressOptions::default() })),
run(expected, None)
);
let options = CompressOptions { target, ..CompressOptions::default() };
test_options(source_text, expected, &options);
}

#[test]
Expand Down
16 changes: 5 additions & 11 deletions crates/oxc_minifier/src/peephole/minimize_conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ impl<'a> PeepholeOptimizations {
mod test {
use crate::{
CompressOptions,
tester::{run, test, test_same},
tester::{test, test_same, test_same_options},
};
use oxc_syntax::es_target::ESTarget;

Expand Down Expand Up @@ -1429,11 +1429,8 @@ mod test {
test_same("foo().a || (foo().a = 3)");

let target = ESTarget::ES2019;
let code = "x || (x = 3)";
assert_eq!(
run(code, Some(CompressOptions { target, ..CompressOptions::default() })),
run(code, None)
);
let options = CompressOptions { target, ..CompressOptions::default() };
test_same_options("x || (x = 3)", &options);
}

#[test]
Expand All @@ -1456,11 +1453,8 @@ mod test {
test("var x; x = x || (() => 'a')", "var x; x ||= (() => 'a')");

let target = ESTarget::ES2019;
let code = "var x; x = x || 1";
assert_eq!(
run(code, Some(CompressOptions { target, ..CompressOptions::default() })),
run(code, None)
);
let options = CompressOptions { target, ..CompressOptions::default() };
test_same_options("var x; x = x || 1", &options);
}

#[test]
Expand Down
23 changes: 2 additions & 21 deletions crates/oxc_minifier/src/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use oxc_semantic::ScopeId;
use oxc_span::{ContentEq, GetSpan};
use oxc_traverse::Ancestor;

use crate::{CompressOptionsUnused, ctx::Ctx, keep_var::KeepVar};
use crate::{ctx::Ctx, keep_var::KeepVar};

use super::{PeepholeOptimizations, State};

Expand Down Expand Up @@ -385,7 +385,7 @@ impl<'a> PeepholeOptimizations {
}
let VariableDeclaration { span, kind, declarations, declare } = var_decl.unbox();
for mut decl in declarations {
if Self::is_declarator_unused(&decl, ctx) {
if Self::should_remove_unused_declarator(&decl, ctx) {
state.changed = true;
if let Some(init) = decl.init.take() {
if init.may_have_side_effects(ctx) {
Expand All @@ -406,25 +406,6 @@ impl<'a> PeepholeOptimizations {
}
}

fn is_declarator_unused(decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
if ctx.state.options.unused == CompressOptionsUnused::Keep {
return false;
}
// It is unsafe to remove if direct eval is involved.
if ctx.scoping().root_scope_flags().contains_direct_eval() {
return false;
}
if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind {
if let Some(symbol_id) = ident.symbol_id.get() {
return ctx
.scoping()
.get_resolved_references(symbol_id)
.all(|r| !r.flags().is_read());
}
}
false
}

fn handle_expression_statement(
&self,
mut expr_stmt: Box<'a, ExpressionStatement<'a>>,
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_minifier/src/peephole/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod minimize_statements;
mod normalize;
mod remove_dead_code;
mod remove_unused_expression;
mod remove_unused_variable_declaration;
mod replace_known_methods;
mod substitute_alternate_syntax;

Expand Down
57 changes: 2 additions & 55 deletions crates/oxc_minifier/src/peephole/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use oxc_ecmascript::{constant_evaluation::ConstantEvaluation, side_effects::MayH
use oxc_span::GetSpan;
use oxc_traverse::Ancestor;

use crate::{CompressOptionsUnused, ctx::Ctx, keep_var::KeepVar};
use crate::{ctx::Ctx, keep_var::KeepVar};

use super::{LatePeepholeOptimizations, PeepholeOptimizations, State};

Expand Down Expand Up @@ -49,42 +49,13 @@ impl<'a> PeepholeOptimizations {
self.try_fold_conditional_expression(e, state, ctx)
}
Expression::SequenceExpression(e) => self.try_fold_sequence_expression(e, state, ctx),
Expression::AssignmentExpression(e) => self.remove_dead_assignment_expression(e, ctx),
_ => None,
} {
*expr = folded_expr;
state.changed = true;
}
}

fn remove_dead_assignment_expression(
&self,
e: &mut AssignmentExpression<'a>,
ctx: &mut Ctx<'a, '_>,
) -> Option<Expression<'a>> {
if matches!(
ctx.state.options.unused,
CompressOptionsUnused::Keep | CompressOptionsUnused::KeepAssign
) {
return None;
}
let SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) =
e.left.as_simple_assignment_target()?
else {
return None;
};
let reference_id = ident.reference_id.get()?;
let symbol_id = ctx.scoping().get_reference(reference_id).symbol_id()?;
// Keep error for assigning to `const foo = 1; foo = 2`.
if ctx.scoping().symbol_flags(symbol_id).is_const_variable() {
return None;
}
if !ctx.scoping().get_resolved_references(symbol_id).all(|r| !r.flags().is_read()) {
return None;
}
Some(e.right.take_in(ctx.ast))
}

/// Removes dead code thats comes after `return`, `throw`, `continue` and `break` statements.
pub fn remove_dead_code_exit_statements(
&self,
Expand Down Expand Up @@ -566,21 +537,6 @@ impl<'a> PeepholeOptimizations {
_ => false,
}
}

fn remove_unused_function_declaration(
f: &Function<'a>,
ctx: &mut Ctx<'a, '_>,
) -> Option<Statement<'a>> {
if ctx.state.options.unused == CompressOptionsUnused::Keep {
return None;
}
let id = f.id.as_ref()?;
let symbol_id = id.symbol_id.get()?;
if ctx.scoping().symbol_is_unused(symbol_id) {
return Some(ctx.ast.statement_empty(f.span));
}
None
}
}

impl<'a> LatePeepholeOptimizations {
Expand All @@ -603,10 +559,7 @@ impl<'a> LatePeepholeOptimizations {
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeRemoveDeadCodeTest.java>
#[cfg(test)]
mod test {
use crate::{
CompressOptions,
tester::{test, test_options, test_same},
};
use crate::tester::{test, test_same};

#[test]
fn test_fold_block() {
Expand Down Expand Up @@ -813,10 +766,4 @@ mod test {
fn remove_constant_value() {
test("const foo = false; if (foo) { console.log('foo') }", "const foo = !1;");
}

#[test]
fn remove_unused_function_declaration() {
let options = CompressOptions::smallest();
test_options("function foo() {}", "", &options);
}
}
3 changes: 3 additions & 0 deletions crates/oxc_minifier/src/peephole/remove_unused_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ impl<'a> PeepholeOptimizations {
Expression::ConditionalExpression(_) => self.fold_conditional_expression(e, state, ctx),
Expression::BinaryExpression(_) => self.fold_binary_expression(e, state, ctx),
Expression::CallExpression(_) => self.fold_call_expression(e, state, ctx),
Expression::AssignmentExpression(_) => {
self.remove_unused_assignment_expression(e, state, ctx)
}
_ => !e.may_have_side_effects(ctx),
}
}
Expand Down
152 changes: 152 additions & 0 deletions crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use oxc_ecmascript::side_effects::MayHaveSideEffects;

use crate::{CompressOptionsUnused, ctx::Ctx};

use super::{PeepholeOptimizations, State};

impl<'a> PeepholeOptimizations {
pub fn should_remove_unused_declarator(
decl: &VariableDeclarator<'a>,
ctx: &mut Ctx<'a, '_>,
) -> bool {
if ctx.state.options.unused == CompressOptionsUnused::Keep {
return false;
}
if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind {
if Self::keep_top_level_var_in_script_mode(ctx) {
return false;
}
// It is unsafe to remove if direct eval is involved.
if ctx.scoping().root_scope_flags().contains_direct_eval() {
return false;
}
if let Some(symbol_id) = ident.symbol_id.get() {
return ctx.scoping().symbol_is_unused(symbol_id);
}
}
false
}

pub fn remove_unused_function_declaration(
f: &Function<'a>,
ctx: &mut Ctx<'a, '_>,
) -> Option<Statement<'a>> {
if ctx.state.options.unused == CompressOptionsUnused::Keep {
return None;
}
if Self::keep_top_level_var_in_script_mode(ctx) {
return None;
}
let id = f.id.as_ref()?;
let symbol_id = id.symbol_id.get()?;
if ctx.scoping().symbol_is_unused(symbol_id) {
return Some(ctx.ast.statement_empty(f.span));
}
None
}

pub fn remove_unused_assignment_expression(
&self,
e: &mut Expression<'a>,
state: &mut State,
ctx: &mut Ctx<'a, '_>,
) -> bool {
let Expression::AssignmentExpression(assign_expr) = e else { return false };
if matches!(
ctx.state.options.unused,
CompressOptionsUnused::Keep | CompressOptionsUnused::KeepAssign
) {
return false;
}
let Some(SimpleAssignmentTarget::AssignmentTargetIdentifier(ident)) =
assign_expr.left.as_simple_assignment_target()
else {
return false;
};
if Self::keep_top_level_var_in_script_mode(ctx) {
return false;
}
let Some(reference_id) = ident.reference_id.get() else { return false };
let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else {
return false;
};
// Keep error for assigning to `const foo = 1; foo = 2`.
if ctx.scoping().symbol_flags(symbol_id).is_const_variable() {
return false;
}
if !ctx.scoping().get_resolved_references(symbol_id).all(|r| !r.flags().is_read()) {
return false;
}
state.changed = true;
if assign_expr.right.may_have_side_effects(ctx) {
*e = assign_expr.right.take_in(ctx.ast);
false
} else {
true
}
}

/// Do remove top level vars in script mode.
fn keep_top_level_var_in_script_mode(ctx: &Ctx<'a, '_>) -> bool {
ctx.scoping.current_scope_id() == ctx.scoping().root_scope_id()
&& ctx.source_type().is_script()
}
}

#[cfg(test)]
mod test {
use oxc_span::SourceType;

use crate::{
CompressOptions,
tester::{
test_options, test_options_source_type, test_same_options,
test_same_options_source_type,
},
};

#[test]
fn remove_unused_variable_declaration() {
let options = CompressOptions::smallest();
test_options("var x", "", &options);
test_options("var x = 1", "", &options);
test_options("var x = foo", "foo", &options);
test_same_options("var x; foo(x)", &options);
test_same_options("export var x", &options);
}

#[test]
fn remove_unused_function_declaration() {
let options = CompressOptions::smallest();
test_options("function foo() {}", "", &options);
test_same_options("function foo() {} foo()", &options);
test_same_options("export function foo() {} foo()", &options);
}

#[test]
fn remove_unused_assignment_expression() {
let options = CompressOptions::smallest();
test_options("var x = 1; x = 2;", "", &options);
test_options("var x = 1; x = 2;", "", &options);
test_options("var x = 1; x = foo();", "foo()", &options);
test_same_options("var x = 1; x = 2, foo(x)", &options);
test_same_options("function foo() { var t; return t = x(); } foo();", &options);
}

#[test]
fn keep_in_script_mode() {
let options = CompressOptions::smallest();
let source_type = SourceType::cjs();
test_same_options_source_type("var x = 1; x = 2;", source_type, &options);
test_same_options_source_type("var x = 1; x = 2, foo(x)", source_type, &options);

test_options_source_type(
"function foo() { var x = 1; x = 2; bar() } foo()",
"function foo() { bar() } foo()",
source_type,
&options,
);
}
}
Loading
Loading