mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(minifier): compress a || (a = b) to a ||= b (#8315)
Compress `a || (a = b)` to `a ||= b` and for other logical operators that are possible to. I didn't find other minifiers doing this, but this is safe for identifiers. [Quoting MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR_assignment#description): > Logical OR assignment short-circuits, meaning that `x ||= y` is equivalent to `x || (x = y)`, except that the expression x is only evaluated once. I actually checked the spec and the only difference was that `Let lRef be ? Evaluation of LeftHandSideExpression` was done twice. Evaluating an IdentifierReference is idempotent so it should be safe to do this compression. References: - [Spec of `&&=`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-assignment-operators:~:text=Return%20r.-,AssignmentExpression,7.%20Return%20rVal.,-AssignmentExpression) - [Spec of `=`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#prod-AssignmentExpression:~:text=Runtime%20Semantics%3A%20Evaluation-,AssignmentExpression,6.%20Return%20rVal.,-AssignmentExpression) - [Spec of `&&`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-binary-logical-operators:~:text=Runtime%20Semantics%3A%20Evaluation-,LogicalANDExpression,5.%20Return%20%3F%C2%A0GetValue(rRef).,-LogicalORExpression) I think this is safe for `a.b || (a.b = foo)` as well because the number of `GetValue` and `SetValue` does not change, but I didn't include that in this PR.
This commit is contained in:
parent
e6fe84d674
commit
ec88c68c28
3 changed files with 74 additions and 10 deletions
|
|
@ -153,7 +153,8 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
|
|||
Expression::AssignmentExpression(e) => {
|
||||
Self::try_compress_assignment_to_update_expression(e, ctx)
|
||||
}
|
||||
Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx),
|
||||
Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx)
|
||||
.or_else(|| self.try_compress_logical_expression_to_assignment_expression(e, ctx)),
|
||||
Expression::NewExpression(e) => Self::try_fold_new_expression(e, ctx),
|
||||
Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx),
|
||||
Expression::BinaryExpression(e) => Self::try_fold_loose_equals_undefined(e, ctx)
|
||||
|
|
@ -457,6 +458,39 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
|
|||
Some(ctx.ast.expression_binary(span, left_id_expr, replace_op, null_expr))
|
||||
}
|
||||
|
||||
/// Compress `a || (a = b)` to `a ||= b`
|
||||
fn try_compress_logical_expression_to_assignment_expression(
|
||||
&self,
|
||||
expr: &mut LogicalExpression<'a>,
|
||||
ctx: Ctx<'a, 'b>,
|
||||
) -> Option<Expression<'a>> {
|
||||
if self.target < ESTarget::ES2020 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Expression::AssignmentExpression(assignment_expr) = &mut expr.right else {
|
||||
return None;
|
||||
};
|
||||
if assignment_expr.operator != AssignmentOperator::Assign {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_op = expr.operator.to_assignment_operator();
|
||||
|
||||
let AssignmentTarget::AssignmentTargetIdentifier(write_id_ref) = &mut assignment_expr.left
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let Expression::Identifier(read_id_ref) = &mut expr.left else { return None };
|
||||
if write_id_ref.name != read_id_ref.name {
|
||||
return None;
|
||||
}
|
||||
|
||||
assignment_expr.span = expr.span;
|
||||
assignment_expr.operator = new_op;
|
||||
Some(ctx.ast.move_expression(&mut expr.right))
|
||||
}
|
||||
|
||||
fn commutative_pair<A, F, G, RetF: 'a, RetG: 'a>(
|
||||
pair: (&A, &A),
|
||||
check_a: F,
|
||||
|
|
@ -1497,6 +1531,27 @@ mod test {
|
|||
test_same("foo !== void 0 && bar !== null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fold_logical_expression_to_assignment_expression() {
|
||||
test("x || (x = 3)", "x ||= 3");
|
||||
test("x && (x = 3)", "x &&= 3");
|
||||
test("x ?? (x = 3)", "x ??= 3");
|
||||
test("x || (x = g())", "x ||= g()");
|
||||
test("x && (x = g())", "x &&= g()");
|
||||
test("x ?? (x = g())", "x ??= g()");
|
||||
|
||||
test_same("x || (y = 3)");
|
||||
|
||||
// foo() might have a side effect
|
||||
test_same("foo().a || (foo().a = 3)");
|
||||
|
||||
let allocator = Allocator::default();
|
||||
let target = ESTarget::ES2019;
|
||||
let mut pass = super::PeepholeSubstituteAlternateSyntax::new(target, false);
|
||||
let code = "x || (x = 3)";
|
||||
tester::test(&allocator, code, code, &mut pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fold_loose_equals_undefined() {
|
||||
test_same("foo != null");
|
||||
|
|
|
|||
|
|
@ -444,6 +444,15 @@ impl LogicalOperator {
|
|||
Self::Coalesce => Precedence::Conditional,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get [`AssignmentOperator`] corresponding to this [`LogicalOperator`].
|
||||
pub fn to_assignment_operator(self) -> AssignmentOperator {
|
||||
match self {
|
||||
Self::Or => AssignmentOperator::LogicalOr,
|
||||
Self::And => AssignmentOperator::LogicalAnd,
|
||||
Self::Coalesce => AssignmentOperator::LogicalNullish,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPrecedence for LogicalOperator {
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Fixture
|
|||
|
||||
173.90 kB | 59.78 kB | 59.82 kB | 19.42 kB | 19.33 kB | moment.js
|
||||
|
||||
287.63 kB | 90.05 kB | 90.07 kB | 32.07 kB | 31.95 kB | jquery.js
|
||||
287.63 kB | 90.04 kB | 90.07 kB | 32.06 kB | 31.95 kB | jquery.js
|
||||
|
||||
342.15 kB | 118.16 kB | 118.14 kB | 44.52 kB | 44.37 kB | vue.js
|
||||
342.15 kB | 118.15 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js
|
||||
|
||||
544.10 kB | 71.79 kB | 72.48 kB | 26.18 kB | 26.20 kB | lodash.js
|
||||
544.10 kB | 71.75 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js
|
||||
|
||||
555.77 kB | 272.95 kB | 270.13 kB | 90.94 kB | 90.80 kB | d3.js
|
||||
555.77 kB | 272.91 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js
|
||||
|
||||
1.01 MB | 460.24 kB | 458.89 kB | 126.84 kB | 126.71 kB | bundle.min.js
|
||||
1.01 MB | 460.22 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js
|
||||
|
||||
1.25 MB | 652.57 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js
|
||||
1.25 MB | 652.56 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js
|
||||
|
||||
2.14 MB | 726.02 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js
|
||||
2.14 MB | 725.98 kB | 724.14 kB | 180.13 kB | 181.07 kB | victory.js
|
||||
|
||||
3.20 MB | 1.01 MB | 1.01 MB | 331.82 kB | 331.56 kB | echarts.js
|
||||
3.20 MB | 1.01 MB | 1.01 MB | 331.80 kB | 331.56 kB | echarts.js
|
||||
|
||||
6.69 MB | 2.32 MB | 2.31 MB | 492.75 kB | 488.28 kB | antd.js
|
||||
|
||||
10.95 MB | 3.50 MB | 3.49 MB | 909.10 kB | 915.50 kB | typescript.js
|
||||
10.95 MB | 3.50 MB | 3.49 MB | 908.36 kB | 915.50 kB | typescript.js
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue