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
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/oxc_minifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ oxc_allocator = { workspace = true }
oxc_ast = { workspace = true }
oxc_ast_visit = { workspace = true }
oxc_codegen = { workspace = true }
oxc_compat = { workspace = true }
oxc_ecmascript = { workspace = true }
oxc_mangler = { workspace = true }
oxc_parser = { workspace = true }
Expand Down
8 changes: 8 additions & 0 deletions crates/oxc_minifier/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use oxc_syntax::{
use oxc_traverse::Ancestor;

use crate::{options::CompressOptions, state::MinifierState, symbol_value::SymbolValue};
use oxc_compat::ESFeature;

pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>;

Expand Down Expand Up @@ -106,6 +107,13 @@ impl<'a> Ctx<'a, '_> {
&self.0.state.options
}

/// Check if the target engines supports a feature.
///
/// Returns `true` if the feature is supported.
pub fn supports_feature(&self, feature: ESFeature) -> bool {
!self.options().target.has_feature(feature)
}

pub fn source_type(&self) -> SourceType {
self.0.state.source_type
}
Expand Down
20 changes: 9 additions & 11 deletions crates/oxc_minifier/src/options.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
use oxc_syntax::es_target::ESTarget;
use oxc_compat::EngineTargets;

pub use oxc_ecmascript::side_effects::PropertyReadSideEffects;

#[derive(Debug, Clone)]
pub struct CompressOptions {
/// Set desired EcmaScript standard version for output.
/// Engine targets for feature detection.
///
/// e.g.
/// Used to determine which ES features are supported by the target engines
/// and whether transformations can be applied.
///
/// * catch optional binding when >= es2019
/// * `??` operator >= es2020
///
/// Default `ESTarget::ESNext`
pub target: ESTarget,
/// Default: empty (supports all features)
pub target: EngineTargets,

/// Remove `debugger;` statements.
///
Expand Down Expand Up @@ -59,7 +57,7 @@ impl Default for CompressOptions {
impl CompressOptions {
pub fn smallest() -> Self {
Self {
target: ESTarget::ESNext,
target: EngineTargets::default(),
keep_names: CompressOptionsKeepNames::all_false(),
drop_debugger: true,
drop_console: false,
Expand All @@ -73,7 +71,7 @@ impl CompressOptions {

pub fn safest() -> Self {
Self {
target: ESTarget::ESNext,
target: EngineTargets::default(),
keep_names: CompressOptionsKeepNames::all_true(),
drop_debugger: false,
drop_console: false,
Expand All @@ -87,7 +85,7 @@ impl CompressOptions {

pub fn dce() -> Self {
Self {
target: ESTarget::ESNext,
target: EngineTargets::default(),
keep_names: CompressOptionsKeepNames::all_true(),
drop_debugger: false,
drop_console: false,
Expand Down
152 changes: 72 additions & 80 deletions crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use crate::ctx::Ctx;
use oxc_allocator::TakeIn;
use oxc_ast::{NONE, ast::*};
use oxc_compat::ESFeature;
use oxc_ecmascript::{
constant_evaluation::{ConstantEvaluation, ConstantValue},
side_effects::MayHaveSideEffects,
};
use oxc_span::{ContentEq, GetSpan};
use oxc_syntax::es_target::ESTarget;

use crate::ctx::Ctx;

use super::PeepholeOptimizations;

Expand Down Expand Up @@ -299,70 +298,73 @@ impl<'a> PeepholeOptimizations {
}

// Try using the "??" or "?." operators
if ctx.options().target >= ESTarget::ES2020 {
if let Expression::BinaryExpression(test_binary) = &mut expr.test {
if let Some(is_negate) = match test_binary.operator {
BinaryOperator::Inequality => Some(true),
BinaryOperator::Equality => Some(false),
_ => None,
} {
// a == null / a != null / (a = foo) == null / (a = foo) != null
let value_expr_with_id_name = if test_binary.left.is_null() {
if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.right)
.filter(|id| !ctx.is_global_reference(id))
{
Some((id.name, &mut test_binary.right))
} else {
None
}
} else if test_binary.right.is_null() {
if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.left)
.filter(|id| !ctx.is_global_reference(id))
{
Some((id.name, &mut test_binary.left))
} else {
None
}
} else {
None
};
if let Some((target_id_name, value_expr)) = value_expr_with_id_name {
// `a == null ? b : a` -> `a ?? b`
// `a != null ? a : b` -> `a ?? b`
// `(a = foo) == null ? b : a` -> `(a = foo) ?? b`
// `(a = foo) != null ? a : b` -> `(a = foo) ?? b`
let maybe_same_id_expr =
if (ctx.supports_feature(ESFeature::ES2020NullishCoalescingOperator)
|| ctx.supports_feature(ESFeature::ES2020OptionalChaining))
&& let Expression::BinaryExpression(test_binary) = &mut expr.test
&& let Some(is_negate) = match test_binary.operator {
BinaryOperator::Inequality => Some(true),
BinaryOperator::Equality => Some(false),
_ => None,
}
{
// a == null / a != null / (a = foo) == null / (a = foo) != null
let value_expr_with_id_name = if test_binary.left.is_null() {
if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.right)
.filter(|id| !ctx.is_global_reference(id))
{
Some((id.name, &mut test_binary.right))
} else {
None
}
} else if test_binary.right.is_null() {
if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.left)
.filter(|id| !ctx.is_global_reference(id))
{
Some((id.name, &mut test_binary.left))
} else {
None
}
} else {
None
};
if let Some((target_id_name, value_expr)) = value_expr_with_id_name {
if ctx.supports_feature(ESFeature::ES2020NullishCoalescingOperator) {
// `a == null ? b : a` -> `a ?? b`
// `a != null ? a : b` -> `a ?? b`
// `(a = foo) == null ? b : a` -> `(a = foo) ?? b`
// `(a = foo) != null ? a : b` -> `(a = foo) ?? b`
let maybe_same_id_expr =
if is_negate { &mut expr.consequent } else { &mut expr.alternate };
if maybe_same_id_expr.is_specific_id(&target_id_name) {
return Some(ctx.ast.expression_logical(
expr.span,
value_expr.take_in(ctx.ast),
LogicalOperator::Coalesce,
if is_negate {
expr.alternate.take_in(ctx.ast)
} else {
expr.consequent.take_in(ctx.ast)
},
));
}
}
if ctx.supports_feature(ESFeature::ES2020OptionalChaining) {
// "a == null ? undefined : a.b.c[d](e)" => "a?.b.c[d](e)"
// "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)"
// "(a = foo) == null ? undefined : a.b.c[d](e)" => "(a = foo)?.b.c[d](e)"
// "(a = foo) != null ? a.b.c[d](e) : undefined" => "(a = foo)?.b.c[d](e)"
let maybe_undefined_expr =
if is_negate { &expr.alternate } else { &expr.consequent };
if ctx.is_expression_undefined(maybe_undefined_expr) {
let expr_to_inject_optional_chaining =
if is_negate { &mut expr.consequent } else { &mut expr.alternate };
if maybe_same_id_expr.is_specific_id(&target_id_name) {
return Some(ctx.ast.expression_logical(
expr.span,
value_expr.take_in(ctx.ast),
LogicalOperator::Coalesce,
if is_negate {
expr.alternate.take_in(ctx.ast)
} else {
expr.consequent.take_in(ctx.ast)
},
));
}

// "a == null ? undefined : a.b.c[d](e)" => "a?.b.c[d](e)"
// "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)"
// "(a = foo) == null ? undefined : a.b.c[d](e)" => "(a = foo)?.b.c[d](e)"
// "(a = foo) != null ? a.b.c[d](e) : undefined" => "(a = foo)?.b.c[d](e)"
let maybe_undefined_expr =
if is_negate { &expr.alternate } else { &expr.consequent };
if ctx.is_expression_undefined(maybe_undefined_expr) {
let expr_to_inject_optional_chaining =
if is_negate { &mut expr.consequent } else { &mut expr.alternate };
if Self::inject_optional_chaining_if_matched(
&target_id_name,
value_expr,
expr_to_inject_optional_chaining,
ctx,
) {
return Some(expr_to_inject_optional_chaining.take_in(ctx.ast));
}
if Self::inject_optional_chaining_if_matched(
&target_id_name,
value_expr,
expr_to_inject_optional_chaining,
ctx,
) {
return Some(expr_to_inject_optional_chaining.take_in(ctx.ast));
}
}
}
Expand Down Expand Up @@ -590,18 +592,7 @@ impl<'a> PeepholeOptimizations {

#[cfg(test)]
mod test {
use oxc_syntax::es_target::ESTarget;

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

fn test_es2019(source_text: &str, expected: &str) {
let target = ESTarget::ES2019;
let options = CompressOptions { target, ..CompressOptions::default() };
test_options(source_text, expected, &options);
}
use crate::tester::{test, test_same, test_target};

#[test]
fn test_minimize_expr_condition() {
Expand Down Expand Up @@ -663,7 +654,7 @@ mod test {
test("var a; a != null ? a : b", "var a; a ?? b");
test("var a; (a = _a) != null ? a : b", "var a; (a = _a) ?? b");
test("v = a != null ? a : b", "v = a == null ? b : a"); // accessing global `a` may have a getter with side effects
test_es2019("var a; v = a != null ? a : b", "var a; v = a == null ? b : a");
test_target("var a; v = a != null ? a : b", "var a; v = a == null ? b : a", "chrome79");
test("var a; v = a != null ? a.b.c[d](e) : undefined", "var a; v = a?.b.c[d](e)");
test(
"var a; v = (a = _a) != null ? a.b.c[d](e) : undefined",
Expand All @@ -674,9 +665,10 @@ mod test {
"var a, undefined = 1; v = a != null ? a.b.c[d](e) : undefined",
"var a; v = a == null ? 1 : a.b.c[d](e)",
);
test_es2019(
test_target(
"var a; v = a != null ? a.b.c[d](e) : undefined",
"var a; v = a == null ? void 0 : a.b.c[d](e)",
"chrome79",
);
test("v = cmp !== 0 ? cmp : (bar, cmp);", "v = (cmp === 0 && bar, cmp);");
test("v = cmp === 0 ? cmp : (bar, cmp);", "v = (cmp === 0 || bar, cmp);");
Expand Down
23 changes: 7 additions & 16 deletions crates/oxc_minifier/src/peephole/minimize_conditions.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use oxc_compat::ESFeature;
use oxc_ecmascript::{
constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType},
side_effects::MayHaveSideEffects,
};
use oxc_semantic::ReferenceFlags;
use oxc_span::GetSpan;
use oxc_syntax::es_target::ESTarget;

use crate::ctx::Ctx;

Expand Down Expand Up @@ -175,10 +175,9 @@ impl<'a> PeepholeOptimizations {
expr: &mut AssignmentExpression<'a>,
ctx: &mut Ctx<'a, '_>,
) {
if ctx.options().target < ESTarget::ES2020 {
return;
}
if !matches!(expr.operator, AssignmentOperator::Assign) {
if !ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators)
|| !matches!(expr.operator, AssignmentOperator::Assign)
{
return;
}

Expand Down Expand Up @@ -263,11 +262,7 @@ impl<'a> PeepholeOptimizations {
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeMinimizeConditionsTest.java>
#[cfg(test)]
mod test {
use crate::{
CompressOptions,
tester::{test, test_options, test_same, test_same_options},
};
use oxc_syntax::es_target::ESTarget;
use crate::tester::{test, test_same, test_target, test_target_same};

/** Check that removing blocks with 1 child works */
#[test]
Expand Down Expand Up @@ -1406,9 +1401,7 @@ mod test {
// foo() might have a side effect
test_same("foo().a || (foo().a = 3)");

let target = ESTarget::ES2019;
let options = CompressOptions { target, ..CompressOptions::default() };
test_same_options("x || (x = 3)", &options);
test_target_same("x || (x = 3)", "chrome84");

test("x || (a, x = 3)", "x ||= (a, 3)");
test("x && (a, x = 3)", "x &&= (a, 3)");
Expand Down Expand Up @@ -1445,9 +1438,7 @@ mod test {
// Example case: `let f = false; f = f || (() => {}); console.log(f.name)`
test("var x; x = x || (() => 'a')", "var x; x ||= (() => 'a')");

let target = ESTarget::ES2019;
let options = CompressOptions { target, ..CompressOptions::default() };
test_options("var x; x = x || 1", "var x = x || 1", &options);
test_target("var x; x = x || 1", "var x = x || 1", "chrome84");
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use oxc_compat::ESFeature;
use oxc_semantic::ReferenceFlags;
use oxc_span::{ContentEq, GetSpan};
use oxc_syntax::es_target::ESTarget;

use crate::ctx::Ctx;

Expand Down Expand Up @@ -209,7 +209,7 @@ impl<'a> PeepholeOptimizations {
expr: &mut Expression<'a>,
ctx: &mut Ctx<'a, '_>,
) {
if ctx.options().target < ESTarget::ES2020 {
if !ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators) {
return;
}
let Expression::LogicalExpression(e) = expr else { return };
Expand Down
Loading
Loading