mirror of
https://github.com/danbulant/oxc
synced 2026-05-20 04:38:54 +00:00
feat(minifier): compress (a = b) === null || a === undefined to (a = b) == null (#8637)
This commit is contained in:
parent
864b8efe1a
commit
2bcbed2d50
2 changed files with 114 additions and 60 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use oxc_allocator::Vec;
|
use oxc_allocator::{CloneIn, Vec};
|
||||||
use oxc_ast::{ast::*, NONE};
|
use oxc_ast::{ast::*, NONE};
|
||||||
use oxc_ecmascript::{
|
use oxc_ecmascript::{
|
||||||
constant_evaluation::{ConstantEvaluation, ValueType},
|
constant_evaluation::{ConstantEvaluation, ValueType},
|
||||||
|
|
@ -331,6 +331,9 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
/// `foo === null || foo === undefined` => `foo == null`
|
/// `foo === null || foo === undefined` => `foo == null`
|
||||||
/// `foo !== null && foo !== undefined` => `foo != null`
|
/// `foo !== null && foo !== undefined` => `foo != null`
|
||||||
///
|
///
|
||||||
|
/// Also supports `(a = foo.bar) === null || a === undefined` which commonly happens when
|
||||||
|
/// optional chaining is lowered. (`(a=foo.bar)==null`)
|
||||||
|
///
|
||||||
/// This compression assumes that `document.all` is a normal object.
|
/// This compression assumes that `document.all` is a normal object.
|
||||||
/// If that assumption does not hold, this compression is not allowed.
|
/// If that assumption does not hold, this compression is not allowed.
|
||||||
/// - `document.all === null || document.all === undefined` is `false`
|
/// - `document.all === null || document.all === undefined` is `false`
|
||||||
|
|
@ -346,8 +349,8 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
LogicalOperator::Coalesce => return None,
|
LogicalOperator::Coalesce => return None,
|
||||||
};
|
};
|
||||||
if let Some(new_expr) = Self::try_compress_is_null_or_undefined_for_left_and_right(
|
if let Some(new_expr) = Self::try_compress_is_null_or_undefined_for_left_and_right(
|
||||||
&expr.left,
|
&mut expr.left,
|
||||||
&expr.right,
|
&mut expr.right,
|
||||||
expr.span,
|
expr.span,
|
||||||
target_ops,
|
target_ops,
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -360,10 +363,11 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
if left.operator != op {
|
if left.operator != op {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let new_span = Span::new(left.right.span().start, expr.span.end);
|
||||||
Self::try_compress_is_null_or_undefined_for_left_and_right(
|
Self::try_compress_is_null_or_undefined_for_left_and_right(
|
||||||
&left.right,
|
&mut left.right,
|
||||||
&expr.right,
|
&mut expr.right,
|
||||||
Span::new(left.right.span().start, expr.span.end),
|
new_span,
|
||||||
target_ops,
|
target_ops,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
|
|
@ -378,60 +382,93 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_compress_is_null_or_undefined_for_left_and_right(
|
fn try_compress_is_null_or_undefined_for_left_and_right(
|
||||||
left: &Expression<'a>,
|
left: &mut Expression<'a>,
|
||||||
right: &Expression<'a>,
|
right: &mut Expression<'a>,
|
||||||
span: Span,
|
span: Span,
|
||||||
(find_op, replace_op): (BinaryOperator, BinaryOperator),
|
(find_op, replace_op): (BinaryOperator, BinaryOperator),
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, 'b>,
|
||||||
) -> Option<Expression<'a>> {
|
) -> Option<Expression<'a>> {
|
||||||
let pair = Self::commutative_pair(
|
enum LeftPairValueResult {
|
||||||
(&left, &right),
|
Null(Span),
|
||||||
|a| {
|
Undefined,
|
||||||
if let Expression::BinaryExpression(op) = a {
|
}
|
||||||
if op.operator == find_op {
|
|
||||||
return Self::commutative_pair(
|
let (
|
||||||
(&op.left, &op.right),
|
Expression::BinaryExpression(left_binary_expr),
|
||||||
|a_a| a_a.is_null().then_some(a_a.span()),
|
Expression::BinaryExpression(right_binary_expr),
|
||||||
|a_b| {
|
) = (left, right)
|
||||||
if let Expression::Identifier(id) = a_b {
|
else {
|
||||||
Some((a_b.span(), (*id).clone()))
|
return None;
|
||||||
} else {
|
};
|
||||||
None
|
if left_binary_expr.operator != find_op || right_binary_expr.operator != find_op {
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
},
|
|
||||||
|b| {
|
|
||||||
if let Expression::BinaryExpression(op) = b {
|
|
||||||
if op.operator == find_op {
|
|
||||||
return Self::commutative_pair(
|
|
||||||
(&op.left, &op.right),
|
|
||||||
|b_a| b_a.evaluate_to_undefined().then_some(()),
|
|
||||||
|b_b| {
|
|
||||||
if let Expression::Identifier(id) = b_b {
|
|
||||||
Some((*id).clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map(|v| v.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let ((null_expr_span, (left_id_expr_span, left_id_ref)), right_id_ref) = pair?;
|
|
||||||
if left_id_ref.name != right_id_ref.name {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let left_id_expr =
|
|
||||||
ctx.ast.expression_identifier_reference(left_id_expr_span, left_id_ref.name);
|
let is_null_or_undefined = |a: &Expression| {
|
||||||
let null_expr = ctx.ast.expression_null_literal(null_expr_span);
|
if a.is_null() {
|
||||||
Some(ctx.ast.expression_binary(span, left_id_expr, replace_op, null_expr))
|
Some(LeftPairValueResult::Null(a.span()))
|
||||||
|
} else if a.evaluate_to_undefined() {
|
||||||
|
Some(LeftPairValueResult::Undefined)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let is_id_or_assign_to_id = |b: &Expression| match b {
|
||||||
|
Expression::Identifier(id) => Some(id.name.clone_in(ctx.ast.allocator)),
|
||||||
|
Expression::AssignmentExpression(assign_expr) => {
|
||||||
|
if assign_expr.operator == AssignmentOperator::Assign {
|
||||||
|
if let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign_expr.left {
|
||||||
|
return Some(id.name.clone_in(ctx.ast.allocator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let (left_value, (left_non_value_expr, left_id_name)) = {
|
||||||
|
let left_value;
|
||||||
|
let left_non_value;
|
||||||
|
if let Some(v) = is_null_or_undefined(&left_binary_expr.left) {
|
||||||
|
left_value = v;
|
||||||
|
let left_non_value_id = is_id_or_assign_to_id(&left_binary_expr.right)?;
|
||||||
|
left_non_value = (&mut left_binary_expr.right, left_non_value_id);
|
||||||
|
} else {
|
||||||
|
left_value = is_null_or_undefined(&left_binary_expr.right)?;
|
||||||
|
let left_non_value_id = is_id_or_assign_to_id(&left_binary_expr.left)?;
|
||||||
|
left_non_value = (&mut left_binary_expr.left, left_non_value_id);
|
||||||
|
}
|
||||||
|
(left_value, left_non_value)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (right_value, right_id) = Self::commutative_pair(
|
||||||
|
(&right_binary_expr.left, &right_binary_expr.right),
|
||||||
|
|a| match left_value {
|
||||||
|
LeftPairValueResult::Null(_) => a.evaluate_to_undefined().then_some(None),
|
||||||
|
LeftPairValueResult::Undefined => a.is_null().then_some(Some(a.span())),
|
||||||
|
},
|
||||||
|
|b| {
|
||||||
|
if let Expression::Identifier(id) = b {
|
||||||
|
Some(id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if left_id_name != right_id.name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let null_expr_span = match left_value {
|
||||||
|
LeftPairValueResult::Null(span) => span,
|
||||||
|
LeftPairValueResult::Undefined => right_value.unwrap(),
|
||||||
|
};
|
||||||
|
Some(ctx.ast.expression_binary(
|
||||||
|
span,
|
||||||
|
ctx.ast.move_expression(left_non_value_expr),
|
||||||
|
replace_op,
|
||||||
|
ctx.ast.expression_null_literal(null_expr_span),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compress `a || (a = b)` to `a ||= b`
|
/// Compress `a || (a = b)` to `a ||= b`
|
||||||
|
|
@ -539,14 +576,14 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commutative_pair<A, F, G, RetF: 'a, RetG: 'a>(
|
fn commutative_pair<'x, A, F, G, RetF: 'x, RetG: 'x>(
|
||||||
pair: (&A, &A),
|
pair: (&'x A, &'x A),
|
||||||
check_a: F,
|
check_a: F,
|
||||||
check_b: G,
|
check_b: G,
|
||||||
) -> Option<(RetF, RetG)>
|
) -> Option<(RetF, RetG)>
|
||||||
where
|
where
|
||||||
F: Fn(&A) -> Option<RetF>,
|
F: Fn(&'x A) -> Option<RetF>,
|
||||||
G: Fn(&A) -> Option<RetG>,
|
G: Fn(&'x A) -> Option<RetG>,
|
||||||
{
|
{
|
||||||
if let Some(a) = check_a(pair.0) {
|
if let Some(a) = check_a(pair.0) {
|
||||||
if let Some(b) = check_b(pair.1) {
|
if let Some(b) = check_b(pair.1) {
|
||||||
|
|
@ -559,6 +596,7 @@ impl<'a, 'b> PeepholeOptimizations {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_fold_loose_equals_undefined(
|
fn try_fold_loose_equals_undefined(
|
||||||
e: &mut BinaryExpression<'a>,
|
e: &mut BinaryExpression<'a>,
|
||||||
ctx: Ctx<'a, 'b>,
|
ctx: Ctx<'a, 'b>,
|
||||||
|
|
@ -1782,6 +1820,22 @@ mod test {
|
||||||
test("foo !== 1 && foo !== null && foo !== void 0", "foo !== 1 && foo != null");
|
test("foo !== 1 && foo !== null && foo !== void 0", "foo !== 1 && foo != null");
|
||||||
test("foo !== 1 || foo !== void 0 && foo !== null", "foo !== 1 || foo != null");
|
test("foo !== 1 || foo !== void 0 && foo !== null", "foo !== 1 || foo != null");
|
||||||
test_same("foo !== void 0 && bar !== null");
|
test_same("foo !== void 0 && bar !== null");
|
||||||
|
|
||||||
|
test("(_foo = foo) === null || _foo === undefined", "(_foo = foo) == null");
|
||||||
|
test("(_foo = foo) === null || _foo === void 0", "(_foo = foo) == null");
|
||||||
|
test("(_foo = foo.bar) === null || _foo === undefined", "(_foo = foo.bar) == null");
|
||||||
|
test("(_foo = foo) !== null && _foo !== undefined", "(_foo = foo) != null");
|
||||||
|
test("(_foo = foo) === undefined || _foo === null", "(_foo = foo) == null");
|
||||||
|
test("(_foo = foo) === void 0 || _foo === null", "(_foo = foo) == null");
|
||||||
|
test(
|
||||||
|
"(_foo = foo) === null || _foo === void 0 || _foo === 1",
|
||||||
|
"(_foo = foo) == null || _foo === 1",
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
"_foo === 1 || (_foo = foo) === null || _foo === void 0",
|
||||||
|
"_foo === 1 || (_foo = foo) == null",
|
||||||
|
);
|
||||||
|
test_same("(_foo = foo) === void 0 || bar === null");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Original | minified | minified | gzip | gzip | Fixture
|
||||||
|
|
||||||
3.20 MB | 1.01 MB | 1.01 MB | 332.00 kB | 331.56 kB | echarts.js
|
3.20 MB | 1.01 MB | 1.01 MB | 332.00 kB | 331.56 kB | echarts.js
|
||||||
|
|
||||||
6.69 MB | 2.31 MB | 2.31 MB | 492.53 kB | 488.28 kB | antd.js
|
6.69 MB | 2.31 MB | 2.31 MB | 491.99 kB | 488.28 kB | antd.js
|
||||||
|
|
||||||
10.95 MB | 3.49 MB | 3.49 MB | 907.24 kB | 915.50 kB | typescript.js
|
10.95 MB | 3.48 MB | 3.49 MB | 905.39 kB | 915.50 kB | typescript.js
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue