Skip to content

Commit 63fdb76

Browse files
committed
feat(minifier): compress a.b = a.b + c to a.b += c
1 parent 00626ca commit 63fdb76

File tree

2 files changed

+53
-29
lines changed

2 files changed

+53
-29
lines changed

crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs

+51-27
Original file line numberDiff line numberDiff line change
@@ -481,18 +481,39 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
481481

482482
let new_op = expr.operator.to_assignment_operator();
483483

484-
match (&assignment_expr.left, &expr.left) {
485-
// `a || (a = b)` -> `a ||= b`
484+
if !Self::has_no_side_effect_for_evaluation_same_target(
485+
&assignment_expr.left,
486+
&expr.left,
487+
ctx,
488+
) {
489+
return None;
490+
}
491+
492+
assignment_expr.span = expr.span;
493+
assignment_expr.operator = new_op;
494+
Some(ctx.ast.move_expression(&mut expr.right))
495+
}
496+
497+
/// Returns `true` if the assignment target and expression have no side effect for *evaluation* and points to the same reference.
498+
///
499+
/// Evaluation here means `Evaluation` in the spec.
500+
/// <https://tc39.es/ecma262/multipage/syntax-directed-operations.html#sec-evaluation>
501+
///
502+
/// Matches the following cases:
503+
///
504+
/// - `a`, `a`
505+
/// - `a.b`, `a.b`
506+
/// - `a.#b`, `a.#b`
507+
fn has_no_side_effect_for_evaluation_same_target(
508+
assignment_target: &AssignmentTarget,
509+
expr: &Expression,
510+
ctx: Ctx<'a, 'b>,
511+
) -> bool {
512+
match (&assignment_target, &expr) {
486513
(
487514
AssignmentTarget::AssignmentTargetIdentifier(write_id_ref),
488515
Expression::Identifier(read_id_ref),
489-
) => {
490-
if write_id_ref.name != read_id_ref.name {
491-
return None;
492-
}
493-
}
494-
// `a.b || (a.b = c)` -> `a.b ||= c`
495-
// `a.#b || (a.#b = c)` -> `a.#b ||= c`
516+
) => write_id_ref.name == read_id_ref.name,
496517
(
497518
AssignmentTarget::StaticMemberExpression(_),
498519
Expression::StaticMemberExpression(_),
@@ -501,24 +522,17 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
501522
AssignmentTarget::PrivateFieldExpression(_),
502523
Expression::PrivateFieldExpression(_),
503524
) => {
504-
let write_expr = assignment_expr.left.to_member_expression();
505-
let read_expr = expr.left.to_member_expression();
525+
let write_expr = assignment_target.to_member_expression();
526+
let read_expr = expr.to_member_expression();
506527
let Expression::Identifier(write_expr_object_id) = &write_expr.object() else {
507-
return None;
528+
return false;
508529
};
509-
// It should also return None when the reference might refer to a reference value created by a with statement
530+
// It should also return false when the reference might refer to a reference value created by a with statement
510531
// when the minifier supports with statements
511-
if ctx.is_global_reference(write_expr_object_id) || write_expr.content_ne(read_expr)
512-
{
513-
return None;
514-
}
532+
!ctx.is_global_reference(write_expr_object_id) && write_expr.content_eq(read_expr)
515533
}
516-
_ => return None,
534+
_ => false,
517535
}
518-
519-
assignment_expr.span = expr.span;
520-
assignment_expr.operator = new_op;
521-
Some(ctx.ast.move_expression(&mut expr.right))
522536
}
523537

524538
fn commutative_pair<A, F, G, RetF: 'a, RetG: 'a>(
@@ -603,14 +617,12 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
603617
if !matches!(expr.operator, AssignmentOperator::Assign) {
604618
return;
605619
}
606-
let AssignmentTarget::AssignmentTargetIdentifier(write_id_ref) = &mut expr.left else {
607-
return;
608-
};
609620

610621
let Expression::BinaryExpression(binary_expr) = &mut expr.right else { return };
611622
let Some(new_op) = binary_expr.operator.to_assignment_operator() else { return };
612-
let Expression::Identifier(read_id_ref) = &mut binary_expr.left else { return };
613-
if write_id_ref.name != read_id_ref.name {
623+
624+
if !Self::has_no_side_effect_for_evaluation_same_target(&expr.left, &binary_expr.left, ctx)
625+
{
614626
return;
615627
}
616628

@@ -1184,6 +1196,18 @@ mod test {
11841196
test_same("x = g() & x");
11851197

11861198
test_same("x = (x -= 2) ^ x");
1199+
1200+
// GetValue(x) has no sideeffect when x is a resolved identifier
1201+
test("var x; x.y = x.y + 3", "var x; x.y += 3");
1202+
test("var x; x.#y = x.#y + 3", "var x; x.#y += 3");
1203+
test_same("x.y = x.y + 3");
1204+
// this can be compressed if `y` does not have side effect
1205+
test_same("var x; x[y] = x[y] + 3");
1206+
// GetValue(x) has a side effect in this case
1207+
// Example case: `var a = { get b() { console.log('b'); return { get c() { console.log('c') } } } }; a.b.c = a.b.c + 1`
1208+
test_same("var x; x.y.z = x.y.z + 3");
1209+
// This case is not supported, since the minifier does not support with statements
1210+
// test_same("var x; with (z) { x.y || (x.y = 3) }");
11871211
}
11881212

11891213
#[test]

tasks/minsize/minsize.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ Original | minified | minified | gzip | gzip | Fixture
1515

1616
1.01 MB | 460.18 kB | 458.89 kB | 126.76 kB | 126.71 kB | bundle.min.js
1717

18-
1.25 MB | 652.84 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js
18+
1.25 MB | 652.82 kB | 646.76 kB | 163.51 kB | 163.73 kB | three.js
1919

2020
2.14 MB | 726.27 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js
2121

2222
3.20 MB | 1.01 MB | 1.01 MB | 331.80 kB | 331.56 kB | echarts.js
2323

2424
6.69 MB | 2.32 MB | 2.31 MB | 492.65 kB | 488.28 kB | antd.js
2525

26-
10.95 MB | 3.49 MB | 3.49 MB | 907.50 kB | 915.50 kB | typescript.js
26+
10.95 MB | 3.49 MB | 3.49 MB | 907.49 kB | 915.50 kB | typescript.js
2727

0 commit comments

Comments
 (0)