support logical assignment operators (#4813)
authorAlex Lam S.L <alexlamsl@gmail.com>
Mon, 22 Mar 2021 20:59:43 +0000 (20:59 +0000)
committerGitHub <noreply@github.com>
Mon, 22 Mar 2021 20:59:43 +0000 (04:59 +0800)
lib/compress.js
lib/parse.js
test/compress/assignments.js
test/compress/collapse_vars.js
test/ufuzz/index.js

index ce12da2..95cccb5 100644 (file)
@@ -821,35 +821,19 @@ merge(Compressor.prototype, {
             } else if (!(left instanceof AST_Destructured || left instanceof AST_SymbolRef)) {
                 mark_assignment_to_arguments(left);
                 return;
-            } else if (node.operator == "=") {
-                node.right.walk(tw);
-                scan_declaration(tw, compressor, left, function() {
-                    return node.right;
-                }, function(sym, fixed, walk) {
-                    if (!(sym instanceof AST_SymbolRef)) {
-                        mark_assignment_to_arguments(sym);
-                        walk();
-                        return;
-                    }
-                    var d = sym.definition();
-                    d.assignments++;
-                    if (fixed
-                        && !is_modified(compressor, tw, node, node.right, 0)
-                        && !sym.in_arg
-                        && safe_to_assign(tw, d)) {
-                        push_ref(d, sym);
-                        mark(tw, d);
-                        if (d.single_use && left instanceof AST_Destructured) d.single_use = false;
-                        tw.loop_ids[d.id] = tw.in_loop;
-                        mark_escaped(tw, d, sym.scope, node, node.right, 0, 1);
-                        sym.fixed = d.fixed = fixed;
-                        sym.fixed.assigns = [ node ];
-                    } else {
-                        walk();
-                        d.fixed = false;
-                    }
-                });
-            } else {
+            } else switch (node.operator) {
+              case "=":
+                walk_assign();
+                break;
+              case "&&=":
+              case "||=":
+              case "??=":
+                left.walk(tw);
+                push(tw);
+                walk_assign();
+                pop(tw);
+                break;
+              default:
                 var d = left.definition();
                 d.assignments++;
                 var fixed = d.fixed;
@@ -900,6 +884,36 @@ merge(Compressor.prototype, {
                     lhs.walk(tw);
                 }
             }
+
+            function walk_assign() {
+                node.right.walk(tw);
+                scan_declaration(tw, compressor, left, function() {
+                    return node.right;
+                }, function(sym, fixed, walk) {
+                    if (!(sym instanceof AST_SymbolRef)) {
+                        mark_assignment_to_arguments(sym);
+                        walk();
+                        return;
+                    }
+                    var d = sym.definition();
+                    d.assignments++;
+                    if (fixed
+                        && !is_modified(compressor, tw, node, node.right, 0)
+                        && !sym.in_arg
+                        && safe_to_assign(tw, d)) {
+                        push_ref(d, sym);
+                        mark(tw, d);
+                        if (d.single_use && left instanceof AST_Destructured) d.single_use = false;
+                        tw.loop_ids[d.id] = tw.in_loop;
+                        mark_escaped(tw, d, sym.scope, node, node.right, 0, 1);
+                        sym.fixed = d.fixed = fixed;
+                        sym.fixed.assigns = [ node ];
+                    } else {
+                        walk();
+                        d.fixed = false;
+                    }
+                });
+            }
         });
         def(AST_Binary, function(tw) {
             if (!lazy_op[this.operator]) return;
@@ -1979,7 +1993,9 @@ merge(Compressor.prototype, {
 
             function should_stop(node, parent) {
                 if (node === rvalue) return true;
-                if (parent instanceof AST_For) return node !== parent.init;
+                if (parent instanceof AST_For) {
+                    if (node !== parent.init) return true;
+                }
                 if (node instanceof AST_Assign) {
                     return node.operator != "=" && lhs.equivalent_to(node.left);
                 }
@@ -2014,7 +2030,8 @@ merge(Compressor.prototype, {
             }
 
             function in_conditional(node, parent) {
-                if (parent instanceof AST_Binary) return lazy_op[parent.operator] && parent.left !== node;
+                if (parent instanceof AST_Assign) return parent.left !== node && lazy_op[parent.operator.slice(0, -1)];
+                if (parent instanceof AST_Binary) return parent.left !== node && lazy_op[parent.operator];
                 if (parent instanceof AST_Case) return parent.expression !== node;
                 if (parent instanceof AST_Conditional) return parent.condition !== node;
                 return parent instanceof AST_If && parent.condition !== node;
@@ -7211,11 +7228,10 @@ merge(Compressor.prototype, {
                 if (compressor.has_directive("use strict") && expr.is_constant()) return this;
             }
             if (left.has_side_effects(compressor)) return this;
-            this.write_only = true;
-            if (root_expr(left).is_constant_expression(compressor.find_parent(AST_Scope))) {
-                return this.right.drop_side_effect_free(compressor);
-            }
-            return this;
+            var right = this.right;
+            this.write_only = !(lazy_op[this.operator.slice(0, -1)] && right.has_side_effects(compressor));
+            if (!root_expr(left).is_constant_expression(compressor.find_parent(AST_Scope))) return this;
+            return right.drop_side_effect_free(compressor);
         });
         def(AST_Await, function(compressor) {
             if (!compressor.option("awaits")) return this;
index b5abf0d..c3ae96d 100644 (file)
@@ -104,12 +104,15 @@ var OPERATORS = makePredicate([
     ">>=",
     "<<=",
     ">>>=",
+    "&=",
     "|=",
     "^=",
-    "&=",
     "&&",
     "||",
     "??",
+    "&&=",
+    "||=",
+    "??=",
 ]);
 
 var NEWLINE_CHARS = "\n\r\u2028\u2029";
@@ -653,7 +656,7 @@ var UNARY_PREFIX = makePredicate("typeof void delete -- ++ ! ~ - +");
 
 var UNARY_POSTFIX = makePredicate("-- ++");
 
-var ASSIGNMENT = makePredicate("= += -= /= *= %= **= >>= <<= >>>= |= ^= &=");
+var ASSIGNMENT = makePredicate("= += -= /= *= %= **= >>= <<= >>>= &= |= ^= &&= ||= ??=");
 
 var PRECEDENCE = function(a, ret) {
     for (var i = 0; i < a.length;) {
index 5bc971a..274a0d6 100644 (file)
@@ -475,3 +475,77 @@ issue_4521: {
     }
     expect_stdout: "42"
 }
+
+logical_assignments: {
+    input: {
+        var a = 42, b = null, c;
+        a &&= "foo";
+        b ||= "bar";
+        c ??= "baz";
+        console.log(a, b, c);
+    }
+    expect_exact: 'var a=42,b=null,c;a&&="foo";b||="bar";c??="baz";console.log(a,b,c);'
+    expect_stdout: "foo bar baz"
+    node_version: ">=15"
+}
+
+logical_collapse_vars: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = "FAIL", b = false;
+        a = "PASS";
+        b ??= a;
+        console.log(a);
+    }
+    expect: {
+        var a = "FAIL", b = false;
+        a = "PASS";
+        b ??= a;
+        console.log(a);
+    }
+    expect_stdout: "PASS"
+    node_version: ">=15"
+}
+
+logical_reduce_vars: {
+    options = {
+        evaluate: true,
+        reduce_vars: true,
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        var a = "PASS", b = 42;
+        b ??= a = "FAIL";
+        console.log(a);
+    }
+    expect: {
+        var a = "PASS", b = 42;
+        b ??= a = "FAIL";
+        console.log(a);
+    }
+    expect_stdout: "PASS"
+    node_version: ">=15"
+}
+
+logical_side_effects: {
+    options = {
+        side_effects: true,
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        var a = "PASS", b = 42;
+        b ??= a = "FAIL";
+        console.log(a);
+    }
+    expect: {
+        var a = "PASS", b = 42;
+        b ??= a = "FAIL";
+        console.log(a);
+    }
+    expect_stdout: "PASS"
+    node_version: ">=15"
+}
index 8530d38..eaff012 100644 (file)
@@ -2868,7 +2868,7 @@ lvalues_def: {
     expect_stdout: true
 }
 
-compound_assignment: {
+compound_assignment_1: {
     options = {
         collapse_vars: true,
     }
@@ -2887,6 +2887,23 @@ compound_assignment: {
     expect_stdout: "4"
 }
 
+compound_assignment_2: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a;
+        a = 1;
+        for (a += a + 2; console.log(a););
+    }
+    expect: {
+        var a;
+        a = 1;
+        for (a += a + 2; console.log(a););
+    }
+    expect_stdout: "4"
+}
+
 issue_2187_1: {
     options = {
         collapse_vars: true,
index d91fe04..e2bcdf2 100644 (file)
@@ -149,6 +149,7 @@ var SUPPORT = function(matrix) {
     for_of: "for (var a of []);",
     generator: "function* f(){}",
     let: "let a;",
+    logical_assignment: "[].p ??= 0;",
     new_target: "function f() { new.target; }",
     nullish: "0 ?? 0",
     rest: "var [...a] = [];",
@@ -262,10 +263,13 @@ ASSIGNMENTS = ASSIGNMENTS.concat([
     ">>=",
     ">>>=",
 ]);
-if (SUPPORT.exponentiation) {
-    ASSIGNMENTS = ASSIGNMENTS.concat(ASSIGNMENTS);
-    ASSIGNMENTS.push("**=");
-}
+ASSIGNMENTS = ASSIGNMENTS.concat(ASSIGNMENTS);
+if (SUPPORT.exponentiation) ASSIGNMENTS.push("**=");
+if (SUPPORT.logical_assignment) ASSIGNMENTS = ASSIGNMENTS.concat([
+    "&&=",
+    "||=",
+    "??=",
+]);
 
 var UNARY_SAFE = [
     "+",