enhance `collapse_vars` (#3613)
authorAlex Lam S.L <alexlamsl@gmail.com>
Fri, 29 Nov 2019 09:45:49 +0000 (17:45 +0800)
committerGitHub <noreply@github.com>
Fri, 29 Nov 2019 09:45:49 +0000 (17:45 +0800)
lib/compress.js
test/compress/collapse_vars.js

index 52c79a7..34700d4 100644 (file)
@@ -1116,7 +1116,7 @@ merge(Compressor.prototype, {
             var args;
             var candidates = [];
             var stat_index = statements.length;
-            var scanner = new TreeTransformer(function(node) {
+            var scanner = new TreeTransformer(function(node, descend) {
                 if (abort) return node;
                 // Skip nodes before `candidate` as quickly as possible
                 if (!hit) {
@@ -1151,11 +1151,16 @@ merge(Compressor.prototype, {
                         return node;
                     }
                     if (is_lhs(node, parent)) {
-                        if (value_def) replaced++;
+                        if (value_def && !hit_rhs) {
+                            assign_used = true;
+                            replaced++;
+                        }
+                        return node;
+                    } else if (value_def) {
+                        if (!hit_rhs) replaced++;
                         return node;
                     } else {
                         replaced++;
-                        if (value_def) return node;
                     }
                     CHANGED = abort = true;
                     AST_Node.info("Collapsing {name} [{file}:{line},{col}]", {
@@ -1188,6 +1193,14 @@ merge(Compressor.prototype, {
                     stop_after = node;
                     if (node instanceof AST_Scope) abort = true;
                 }
+                // Scan but don't replace inside getter/setter
+                if (node instanceof AST_Accessor) {
+                    var replace = can_replace;
+                    can_replace = false;
+                    descend(node, scanner);
+                    can_replace = replace;
+                    return node;
+                }
                 return handle_custom_scan_order(node);
             }, function(node) {
                 if (abort) return;
@@ -1200,9 +1213,28 @@ merge(Compressor.prototype, {
                 if (!hit) {
                     if (node !== hit_stack[hit_index]) return node;
                     hit_index++;
-                    if (hit_index < hit_stack.length) return;
-                    hit = true;
-                    return node;
+                    switch (hit_stack.length - hit_index) {
+                      case 0:
+                        hit = true;
+                        if (assign_used) return node;
+                        if (node instanceof AST_VarDef) return node;
+                        def.replaced++;
+                        var parent = multi_replacer.parent();
+                        if (parent instanceof AST_Sequence && parent.tail_node() !== node) {
+                            value_def.replaced++;
+                            return MAP.skip;
+                        }
+                        return get_rvalue(candidate);
+                      case 1:
+                        if (!assign_used && node.body === candidate) {
+                            hit = true;
+                            def.replaced++;
+                            value_def.replaced++;
+                            return null;
+                        }
+                      default:
+                        return;
+                    }
                 }
                 // Replace variable when found
                 if (node instanceof AST_SymbolRef
@@ -1215,7 +1247,8 @@ merge(Compressor.prototype, {
                 }
                 // Skip (non-executed) functions and (leading) default case in switch statements
                 if (node instanceof AST_Default || node instanceof AST_Scope) return node;
-            });
+            }, patch_sequence);
+            var force_single;
             while (--stat_index >= 0) {
                 // Treat parameters as collapsible in IIFE, i.e.
                 //   function(a, b){ ... }(x());
@@ -1248,7 +1281,10 @@ merge(Compressor.prototype, {
                     } : side_effects_external : return_false;
                     var funarg = candidate.name instanceof AST_SymbolFunarg;
                     var hit = funarg;
-                    var abort = false, replaced = 0, can_replace = !args || !hit;
+                    var abort = false;
+                    var replaced = 0;
+                    var assign_used = false;
+                    var can_replace = !args || !hit;
                     if (!can_replace) {
                         for (var j = compressor.self().argnames.lastIndexOf(candidate.name) + 1; !abort && j < args.length; j++) {
                             args[j].transform(scanner);
@@ -1260,20 +1296,22 @@ merge(Compressor.prototype, {
                     }
                     if (value_def) {
                         var def = lhs.definition();
-                        if (abort) {
-                            var referenced = def.references.length - def.replaced;
-                            if (candidate instanceof AST_Assign) referenced--;
-                            if (referenced > replaced) {
-                                replaced = false;
-                            } else {
-                                abort = false;
-                            }
+                        var referenced = def.references.length - def.replaced;
+                        if (candidate instanceof AST_Assign) referenced--;
+                        if (replaced && referenced == replaced) {
+                            abort = false;
+                        } else if (candidate instanceof AST_Assign) {
+                            candidates.push(hit_stack);
+                            force_single = true;
+                            continue;
+                        } else {
+                            replaced = false;
                         }
-                        if (!abort) {
+                        if (replaced) {
                             hit_index = 0;
                             hit = funarg;
                             for (var i = stat_index; !abort && i < statements.length; i++) {
-                                statements[i].transform(multi_replacer);
+                                if (!statements[i].transform(multi_replacer)) statements.splice(i--, 1);
                             }
                             value_def.single_use = false;
                         }
@@ -1502,9 +1540,12 @@ merge(Compressor.prototype, {
 
             function find_stop(node, level) {
                 var parent = scanner.parent(level);
-                if (parent instanceof AST_Array) return node;
+                if (parent instanceof AST_Array) return value_def ? find_stop(parent, level + 1) : node;
                 if (parent instanceof AST_Assign) return node;
-                if (parent instanceof AST_Binary) return node;
+                if (parent instanceof AST_Binary) {
+                    if (!value_def || parent.left !== node) return node;
+                    return find_stop(parent, level + 1);
+                }
                 if (parent instanceof AST_Call) return node;
                 if (parent instanceof AST_Case) return node;
                 if (parent instanceof AST_Conditional) return node;
@@ -1512,7 +1553,9 @@ merge(Compressor.prototype, {
                 if (parent instanceof AST_Exit) return node;
                 if (parent instanceof AST_If) return node;
                 if (parent instanceof AST_IterationStatement) return node;
-                if (parent instanceof AST_ObjectKeyVal) return node;
+                if (parent instanceof AST_ObjectKeyVal) {
+                    return value_def ? find_stop(scanner.parent(level + 1), level + 2) : node;
+                }
                 if (parent instanceof AST_PropAccess) return node;
                 if (parent instanceof AST_Sequence) {
                     return (parent.tail_node() === node ? find_stop : find_stop_unused)(parent, level + 1);
@@ -1557,8 +1600,11 @@ merge(Compressor.prototype, {
                 return null;
             }
 
-            function mangleable_var(var_def) {
-                var value = var_def.value;
+            function mangleable_var(value) {
+                if (force_single) {
+                    force_single = false;
+                    return;
+                }
                 if (!(value instanceof AST_SymbolRef)) return;
                 var def = value.definition();
                 if (def.undeclared) return;
@@ -1573,11 +1619,21 @@ merge(Compressor.prototype, {
                     var referenced = def.references.length - def.replaced;
                     var declared = def.orig.length - def.eliminated;
                     if (declared > 1 && !(expr.name instanceof AST_SymbolFunarg)
-                        || (referenced > 1 ? mangleable_var(expr) : !compressor.exposed(def))) {
+                        || (referenced > 1 ? mangleable_var(expr.value) : !compressor.exposed(def))) {
                         return make_node(AST_SymbolRef, expr.name, expr.name);
                     }
+                } else if (expr instanceof AST_Assign) {
+                    var lhs = expr.left;
+                    if (expr.operator == "=" && lhs instanceof AST_SymbolRef) {
+                        var def = lhs.definition();
+                        if (def.references[0] === lhs) {
+                            var referenced = def.references.length - def.replaced;
+                            if (referenced > 1) mangleable_var(expr.right);
+                        }
+                    }
+                    return lhs;
                 } else {
-                    return expr[expr instanceof AST_Assign ? "left" : "expression"];
+                    return expr.expression;
                 }
             }
 
@@ -1689,20 +1745,15 @@ merge(Compressor.prototype, {
                         node.value = null;
                         return node;
                     }
-                    var parent = this.parent();
-                    if (!parent) return in_list ? MAP.skip : null;
-                    if (parent instanceof AST_Sequence) return MAP.skip;
-                    var value = expr;
-                    do {
-                        value = get_rvalue(value);
-                    } while (value instanceof AST_Assign);
-                    return value;
-                }, function(node) {
-                    if (node instanceof AST_Sequence) switch (node.expressions.length) {
-                      case 0: return null;
-                      case 1: return node.expressions[0];
-                    }
-                }));
+                    return in_list ? MAP.skip : null;
+                }, patch_sequence));
+            }
+
+            function patch_sequence(node) {
+                if (node instanceof AST_Sequence) switch (node.expressions.length) {
+                    case 0: return null;
+                    case 1: return node.expressions[0];
+                }
             }
 
             function is_lhs_local(lhs) {
index 175ab9c..20bbfea 100644 (file)
@@ -1348,8 +1348,7 @@ collapse_vars_array_3: {
     }
     expect: {
         function f(a) {
-            var b;
-            return [ b = a, b, b ];
+            return [ a, a, a ];
         }
         console.log(f().length);
     }
@@ -1487,11 +1486,10 @@ collapse_vars_object_3: {
     }
     expect: {
         function f(a) {
-            var b;
             return {
-                p: b = a,
-                q: b,
-                r: b,
+                p: a,
+                q: a,
+                r: a,
             };
         }
         console.log(f("PASS").r);
@@ -6304,7 +6302,7 @@ assign_left: {
     }
     expect: {
         console.log(function(a, b) {
-            (b = a).p.q = "PASS";
+            a.p.q = "PASS";
             return a.p.q;
         }({p: {}}));
     }
@@ -6317,12 +6315,12 @@ sub_property: {
     }
     input: {
         console.log(function(a, b) {
-            return a[(b = a, b.length - 1)];
+            return a[b = a, b.length - 1];
         }([ "FAIL", "PASS" ]));
     }
     expect: {
         console.log(function(a, b) {
-            return a[(b = a).length - 1];
+            return a[a.length - 1];
         }([ "FAIL", "PASS" ]));
     }
     expect_stdout: "PASS"
@@ -6754,7 +6752,7 @@ local_value_replacement: {
     }
     expect: {
         function f(a, b) {
-            (a = b) && g(a);
+            b && g(b);
         }
         function g(c) {
             console.log(c);
@@ -6805,3 +6803,165 @@ array_in_object_2: {
     }
     expect_stdout: "1 1"
 }
+
+array_in_conditional: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = 1, b = 2, c;
+        console.log(c && [ b = a ], a, b);
+    }
+    expect: {
+        var a = 1, b = 2, c;
+        console.log(c && [ b = a ], a, b);
+    }
+    expect_stdout: "undefined 1 2"
+}
+
+object_in_conditional: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = 1, b = 2, c;
+        console.log(c && {
+            p: b = a
+        }, a, b);
+    }
+    expect: {
+        var a = 1, b = 2, c;
+        console.log(c && {
+            p: b = a
+        }, a, b);
+    }
+    expect_stdout: "undefined 1 2"
+}
+
+sequence_in_iife_1: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = "foo", b = 42;
+        (function() {
+            var c = (b = a, b);
+        })();
+        console.log(a, b);
+    }
+    expect: {
+        var a = "foo", b = 42;
+        (function() {
+            var c = b = a;
+        })();
+        console.log(a, b);
+    }
+    expect_stdout: "foo foo"
+}
+
+sequence_in_iife_2: {
+    options = {
+        collapse_vars: true,
+        inline: true,
+        passes: 2,
+        side_effects: true,
+        unused: true,
+    }
+    input: {
+        var a = "foo", b = 42;
+        (function() {
+            var c = (b = a, b);
+        })();
+        console.log(a, b);
+    }
+    expect: {
+        var a = "foo", b = 42;
+        console.log(a, a);
+    }
+    expect_stdout: "foo foo"
+}
+
+retain_assign: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = 42, b, c = "FAIL";
+        b = a;
+        b++ && (c = "PASS");
+        console.log(c);
+    }
+    expect: {
+        var a = 42, b, c = "FAIL";
+        b = a;
+        b++ && (c = "PASS");
+        console.log(c);
+    }
+    expect_stdout: "PASS"
+}
+
+getter_side_effect: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var c = "FAIL";
+        (function(a) {
+            var b;
+            (b = a) && {
+                get foo() {
+                    a = 0;
+                }
+            }.foo;
+            b && (c = "PASS");
+        })(42);
+        console.log(c);
+    }
+    expect: {
+        var c = "FAIL";
+        (function(a) {
+            var b;
+            (b = a) && {
+                get foo() {
+                    a = 0;
+                }
+            }.foo;
+            b && (c = "PASS");
+        })(42);
+        console.log(c);
+    }
+    expect_stdout: "PASS"
+}
+
+setter_side_effect: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var c = "FAIL";
+        (function(a) {
+            var b;
+            (b = a) && ({
+                set foo(v) {
+                    a = v;
+                }
+            }.foo = 0);
+            b && (c = "PASS");
+        })(42);
+        console.log(c);
+    }
+    expect: {
+        var c = "FAIL";
+        (function(a) {
+            var b;
+            (b = a) && ({
+                set foo(v) {
+                    a = v;
+                }
+            }.foo = 0);
+            b && (c = "PASS");
+        })(42);
+        console.log(c);
+    }
+    expect_stdout: "PASS"
+}