support spread syntax (#4328)
authorAlex Lam S.L <alexlamsl@gmail.com>
Sat, 5 Dec 2020 21:19:31 +0000 (21:19 +0000)
committerGitHub <noreply@github.com>
Sat, 5 Dec 2020 21:19:31 +0000 (05:19 +0800)
lib/ast.js
lib/compress.js
lib/output.js
lib/parse.js
lib/transform.js
test/compress/spread.js [new file with mode: 0644]
test/reduce.js
test/ufuzz/index.js

index 2a4631f..792264f 100644 (file)
@@ -209,6 +209,8 @@ var AST_EmptyStatement = DEFNODE("EmptyStatement", null, {
 
 function must_be_expression(node, prop) {
     if (!(node[prop] instanceof AST_Node)) throw new Error(prop + " must be AST_Node");
+    if (node[prop] instanceof AST_Hole) throw new Error(prop + " cannot be AST_Hole");
+    if (node[prop] instanceof AST_Spread) throw new Error(prop + " cannot be AST_Spread");
     if (node[prop] instanceof AST_Statement && !(node[prop] instanceof AST_Function)) {
         throw new Error(prop + " cannot be AST_Statement");
     }
@@ -817,9 +819,11 @@ var AST_VarDef = DEFNODE("VarDef", "name value", {
 
 /* -----[ OTHER ]----- */
 
-function must_be_expressions(node, prop) {
+function must_be_expressions(node, prop, allow_spread, allow_hole) {
     node[prop].forEach(function(node) {
         if (!(node instanceof AST_Node)) throw new Error(prop + " must be AST_Node[]");
+        if (!allow_hole && node instanceof AST_Hole) throw new Error(prop + " cannot be AST_Hole");
+        if (!allow_spread && node instanceof AST_Spread) throw new Error(prop + " cannot be AST_Spread");
         if (node instanceof AST_Statement && !(node instanceof AST_Function)) {
             throw new Error(prop + " cannot contain AST_Statement");
         }
@@ -843,7 +847,7 @@ var AST_Call = DEFNODE("Call", "expression args pure", {
     },
     _validate: function() {
         must_be_expression(this, "expression");
-        must_be_expressions(this, "args");
+        must_be_expressions(this, "args", true);
     },
 });
 
@@ -920,6 +924,22 @@ var AST_Sub = DEFNODE("Sub", null, {
     },
 }, AST_PropAccess);
 
+var AST_Spread = DEFNODE("Spread", "expression", {
+    $documentation: "Spread expression in array/object literals or function calls",
+    $propdoc: {
+        expression: "[AST_Node] expression to be expanded",
+    },
+    walk: function(visitor) {
+        var node = this;
+        visitor.visit(node, function() {
+            node.expression.walk(visitor);
+        });
+    },
+    _validate: function() {
+        must_be_expression(this, "expression");
+    },
+});
+
 var AST_Unary = DEFNODE("Unary", "operator expression", {
     $documentation: "Base class for unary expressions",
     $propdoc: {
@@ -1020,7 +1040,7 @@ var AST_Array = DEFNODE("Array", "elements", {
         });
     },
     _validate: function() {
-        must_be_expressions(this, "elements");
+        must_be_expressions(this, "elements", true, true);
     },
 });
 
@@ -1098,7 +1118,7 @@ var AST_DestructuredObject = DEFNODE("DestructuredObject", "properties", {
 var AST_Object = DEFNODE("Object", "properties", {
     $documentation: "An object literal",
     $propdoc: {
-        properties: "[AST_ObjectProperty*] array of properties"
+        properties: "[(AST_ObjectProperty|AST_Spread)*] array of properties"
     },
     walk: function(visitor) {
         var node = this;
@@ -1110,7 +1130,9 @@ var AST_Object = DEFNODE("Object", "properties", {
     },
     _validate: function() {
         this.properties.forEach(function(node) {
-            if (!(node instanceof AST_ObjectProperty)) throw new Error("properties must be AST_ObjectProperty[]");
+            if (!(node instanceof AST_ObjectProperty || node instanceof AST_Spread)) {
+                throw new Error("properties must contain AST_ObjectProperty and/or AST_Spread only");
+            }
         });
     },
 });
index 7434a20..a78cf6e 100644 (file)
@@ -866,7 +866,12 @@ merge(Compressor.prototype, {
             var fn = this;
             fn.inlined = false;
             var iife;
-            if (!fn.name && (iife = tw.parent()) instanceof AST_Call && iife.expression === fn) {
+            if (!fn.name
+                && (iife = tw.parent()) instanceof AST_Call
+                && iife.expression === fn
+                && all(iife.args, function(arg) {
+                    return !(arg instanceof AST_Spread);
+                })) {
                 var hit = false;
                 var aborts = false;
                 fn.walk(new TreeWalker(function(node) {
@@ -1786,6 +1791,7 @@ merge(Compressor.prototype, {
                 if (node instanceof AST_PropAccess) {
                     return side_effects || !value_def && node.expression.may_throw_on_access(compressor);
                 }
+                if (node instanceof AST_Spread) return true;
                 if (node instanceof AST_SymbolRef) {
                     if (symbol_in_lvalues(node, parent)) {
                         return !(parent instanceof AST_Assign && parent.operator == "=" && parent.left === node);
@@ -1819,7 +1825,10 @@ merge(Compressor.prototype, {
                     && !fn.uses_arguments
                     && !fn.pinned()
                     && (iife = compressor.parent()) instanceof AST_Call
-                    && iife.expression === fn) {
+                    && iife.expression === fn
+                    && all(iife.args, function(arg) {
+                        return !(arg instanceof AST_Spread);
+                    })) {
                     var fn_strict = compressor.has_directive("use strict");
                     if (fn_strict && !member(fn_strict, fn.body)) fn_strict = false;
                     var len = fn.argnames.length;
@@ -1932,6 +1941,8 @@ merge(Compressor.prototype, {
                     expr.expressions.forEach(extract_candidates);
                 } else if (expr instanceof AST_SimpleStatement) {
                     extract_candidates(expr.body);
+                } else if (expr instanceof AST_Spread) {
+                    extract_candidates(expr.expression);
                 } else if (expr instanceof AST_Sub) {
                     extract_candidates(expr.expression);
                     extract_candidates(expr.property);
@@ -1978,6 +1989,7 @@ merge(Compressor.prototype, {
                     return (parent.tail_node() === node ? find_stop : find_stop_unused)(parent, level + 1);
                 }
                 if (parent instanceof AST_SimpleStatement) return find_stop_unused(parent, level + 1);
+                if (parent instanceof AST_Spread) return node;
                 if (parent instanceof AST_Switch) return node;
                 if (parent instanceof AST_Unary) return node;
                 if (parent instanceof AST_VarDef) return node;
@@ -2087,6 +2099,7 @@ merge(Compressor.prototype, {
                 }
                 if (parent instanceof AST_Sequence) return find_stop_unused(parent, level + 1);
                 if (parent instanceof AST_SimpleStatement) return find_stop_unused(parent, level + 1);
+                if (parent instanceof AST_Spread) return node;
                 if (parent instanceof AST_Switch) return find_stop_unused(parent, level + 1);
                 if (parent instanceof AST_Unary) return find_stop_unused(parent, level + 1);
                 if (parent instanceof AST_VarDef) {
@@ -4224,15 +4237,19 @@ merge(Compressor.prototype, {
 
     // determine if expression has side effects
     (function(def) {
-        function any(list, compressor) {
-            for (var i = list.length; --i >= 0;)
-                if (list[i].has_side_effects(compressor))
-                    return true;
-            return false;
+        function any(list, compressor, spread) {
+            return !all(list, spread ? function(node) {
+                return node instanceof AST_Spread ? !spread(node, compressor) : !node.has_side_effects(compressor);
+            } : function(node) {
+                return !node.has_side_effects(compressor);
+            });
+        }
+        function array_spread(node, compressor) {
+            return !node.expression.is_string(compressor) || node.expression.has_side_effects(compressor);
         }
         def(AST_Node, return_true);
         def(AST_Array, function(compressor) {
-            return any(this.elements, compressor);
+            return any(this.elements, compressor, array_spread);
         });
         def(AST_Assign, function(compressor) {
             var lhs = this.left;
@@ -4256,7 +4273,7 @@ merge(Compressor.prototype, {
                 && (!this.is_call_pure(compressor) || this.expression.has_side_effects(compressor))) {
                 return true;
             }
-            return any(this.args, compressor);
+            return any(this.args, compressor, array_spread);
         });
         def(AST_Case, function(compressor) {
             return this.expression.has_side_effects(compressor)
@@ -4296,23 +4313,38 @@ merge(Compressor.prototype, {
         });
         def(AST_Lambda, return_false);
         def(AST_Object, function(compressor) {
-            return any(this.properties, compressor);
+            return any(this.properties, compressor, function(node, compressor) {
+                var exp = node.expression;
+                if (exp instanceof AST_Object) return true;
+                if (exp instanceof AST_PropAccess) return true;
+                if (exp instanceof AST_SymbolRef) {
+                    exp = exp.fixed_value();
+                    if (!exp) return true;
+                    if (exp instanceof AST_SymbolRef) return true;
+                    if (exp instanceof AST_PropAccess) return true;
+                    if (!(exp instanceof AST_Object)) return false;
+                    return !all(exp.properties, function(prop) {
+                        return !(prop instanceof AST_ObjectGetter || prop instanceof AST_Spread);
+                    });
+                }
+                return exp.has_side_effects(compressor);
+            });
         });
         def(AST_ObjectProperty, function(compressor) {
             return this.key instanceof AST_Node && this.key.has_side_effects(compressor)
                 || this.value.has_side_effects(compressor);
         });
-        def(AST_Sub, function(compressor) {
-            return this.expression.may_throw_on_access(compressor)
-                || this.expression.has_side_effects(compressor)
-                || this.property.has_side_effects(compressor);
-        });
         def(AST_Sequence, function(compressor) {
             return any(this.expressions, compressor);
         });
         def(AST_SimpleStatement, function(compressor) {
             return this.body.has_side_effects(compressor);
         });
+        def(AST_Sub, function(compressor) {
+            return this.expression.may_throw_on_access(compressor)
+                || this.expression.has_side_effects(compressor)
+                || this.property.has_side_effects(compressor);
+        });
         def(AST_Switch, function(compressor) {
             return this.expression.has_side_effects(compressor)
                 || any(this.body, compressor);
@@ -4612,6 +4644,9 @@ merge(Compressor.prototype, {
                 if (!all(self.argnames, function(argname) {
                     return argname instanceof AST_SymbolFunarg;
                 })) break;
+                if (!all(call.args, function(arg) {
+                    return !(arg instanceof AST_Spread);
+                })) break;
                 for (var j = 0; j < len; j++) {
                     var arg = call.args[j];
                     if (!(arg instanceof AST_SymbolRef)) break;
@@ -6171,26 +6206,43 @@ merge(Compressor.prototype, {
         // Returns an array of expressions with side-effects or null
         // if all elements were dropped. Note: original array may be
         // returned if nothing changed.
-        function trim(nodes, compressor, first_in_statement) {
+        function trim(nodes, compressor, first_in_statement, spread) {
             var len = nodes.length;
             if (!len) return null;
             var ret = [], changed = false;
             for (var i = 0; i < len; i++) {
-                var node = nodes[i].drop_side_effect_free(compressor, first_in_statement);
-                changed |= node !== nodes[i];
-                if (node) {
-                    ret.push(node);
+                var node = nodes[i];
+                var trimmed;
+                if (spread && node instanceof AST_Spread) {
+                    trimmed = spread(node, compressor, first_in_statement);
+                } else {
+                    trimmed = node.drop_side_effect_free(compressor, first_in_statement);
+                }
+                if (trimmed !== node) changed = true;
+                if (trimmed) {
+                    ret.push(trimmed);
                     first_in_statement = false;
                 }
             }
             return changed ? ret.length ? ret : null : nodes;
         }
-
+        function array_spread(node, compressor, first_in_statement) {
+            var exp = node.expression;
+            if (!exp.is_string(compressor)) return node;
+            return exp.drop_side_effect_free(compressor, first_in_statement);
+        }
         def(AST_Node, return_this);
         def(AST_Accessor, return_null);
         def(AST_Array, function(compressor, first_in_statement) {
-            var values = trim(this.elements, compressor, first_in_statement);
-            return values && make_sequence(this, values);
+            var values = trim(this.elements, compressor, first_in_statement, array_spread);
+            if (!values) return null;
+            if (all(values, function(node) {
+                return !(node instanceof AST_Spread);
+            })) return make_sequence(this, values);
+            if (values === this.elements) return this;
+            var node = this.clone();
+            node.elements = values;
+            return node;
         });
         def(AST_Assign, function(compressor) {
             var left = this.left;
@@ -6241,14 +6293,14 @@ merge(Compressor.prototype, {
             var self = this;
             if (self.is_expr_pure(compressor)) {
                 if (self.pure) AST_Node.warn("Dropping __PURE__ call [{file}:{line},{col}]", self.start);
-                var args = trim(self.args, compressor, first_in_statement);
+                var args = trim(self.args, compressor, first_in_statement, array_spread);
                 return args && make_sequence(self, args);
             }
             var exp = self.expression;
             if (self.is_call_pure(compressor)) {
                 var exprs = self.args.slice();
                 exprs.unshift(exp.expression);
-                exprs = trim(exprs, compressor, first_in_statement);
+                exprs = trim(exprs, compressor, first_in_statement, array_spread);
                 return exprs && make_sequence(self, exprs);
             }
             var def;
@@ -6279,7 +6331,7 @@ merge(Compressor.prototype, {
                     if (assign_this_only) {
                         var exprs = self.args.slice();
                         exprs.unshift(exp);
-                        exprs = trim(exprs, compressor, first_in_statement);
+                        exprs = trim(exprs, compressor, first_in_statement, array_spread);
                         return exprs && make_sequence(self, exprs);
                     }
                     if (!fn.contains_this()) return make_node(AST_Call, self, self);
@@ -6337,11 +6389,38 @@ merge(Compressor.prototype, {
         def(AST_Object, function(compressor, first_in_statement) {
             var exprs = [];
             this.properties.forEach(function(prop) {
-                if (prop.key instanceof AST_Node) exprs.push(prop.key);
-                exprs.push(prop.value);
+                if (prop instanceof AST_Spread) {
+                    exprs.push(prop);
+                } else {
+                    if (prop.key instanceof AST_Node) exprs.push(prop.key);
+                    exprs.push(prop.value);
+                }
+            });
+            var values = trim(exprs, compressor, first_in_statement, function(node, compressor, first_in_statement) {
+                var exp = node.expression;
+                if (exp instanceof AST_Object) return node;
+                if (exp instanceof AST_PropAccess) return node;
+                if (exp instanceof AST_SymbolRef) {
+                    exp = exp.fixed_value();
+                    if (!exp) return node;
+                    if (exp instanceof AST_SymbolRef) return node;
+                    if (exp instanceof AST_PropAccess) return node;
+                    if (!(exp instanceof AST_Object)) return null;
+                    return all(exp.properties, function(prop) {
+                        return !(prop instanceof AST_ObjectGetter || prop instanceof AST_Spread);
+                    }) ? null : node;
+                }
+                return exp.drop_side_effect_free(compressor, first_in_statement);
             });
-            var values = trim(exprs, compressor, first_in_statement);
-            return values && make_sequence(this, values);
+            if (!values) return null;
+            if (values === exprs && !all(values, function(node) {
+                return !(node instanceof AST_Spread);
+            })) return this;
+            return make_sequence(this, values.map(function(node) {
+                return node instanceof AST_Spread ? make_node(AST_Object, node, {
+                    properties: [ node ],
+                }) : node;
+            }));
         });
         def(AST_Sequence, function(compressor, first_in_statement) {
             var expressions = trim(this.expressions, compressor, first_in_statement);
@@ -7213,6 +7292,9 @@ merge(Compressor.prototype, {
         if (fn.pinned()) return;
         if (fns_with_marked_args && fns_with_marked_args.indexOf(fn) < 0) return;
         var args = call.args;
+        if (!all(args, function(arg) {
+            return !(arg instanceof AST_Spread);
+        })) return;
         var pos = 0, last = 0;
         var drop_fargs = fn === exp && !fn.name && compressor.drop_fargs(fn, call) ? function(argname, arg) {
             if (!argname) return true;
@@ -7566,6 +7648,9 @@ merge(Compressor.prototype, {
             && !self.is_expr_pure(compressor)
             && all(fn.argnames, function(argname) {
                 return !(argname instanceof AST_Destructured);
+            })
+            && all(self.args, function(arg) {
+                return !(arg instanceof AST_Spread);
             });
         if (can_inline && stat instanceof AST_Return) {
             var value = stat.value;
@@ -7630,7 +7715,12 @@ merge(Compressor.prototype, {
                     return !(argname instanceof AST_Destructured);
                 })
                 && (fn !== exp || fn_name_unused(fn, compressor))) {
-                var args = self.args.concat(make_node(AST_Undefined, self));
+                var args = self.args.map(function(arg) {
+                    return arg instanceof AST_Spread ? make_node(AST_Array, arg, {
+                        elements: [ arg ],
+                    }) : arg;
+                });
+                args.push(make_node(AST_Undefined, self));
                 return make_sequence(self, args).optimize(compressor);
             }
         }
@@ -9599,6 +9689,20 @@ merge(Compressor.prototype, {
         });
     });
 
+    OPT(AST_Spread, function(self, compressor) {
+        if (compressor.option("properties")) {
+            var exp = self.expression;
+            if (compressor.parent() instanceof AST_Object) {
+                if (exp instanceof AST_Object && all(exp.properties, function(node) {
+                    return node instanceof AST_ObjectKeyVal;
+                })) return List.splice(exp.properties);
+            } else if (exp instanceof AST_Array) return List.splice(exp.elements.map(function(node) {
+                return node instanceof AST_Hole ? make_node(AST_Undefined, node).optimize(compressor) : node;
+            }));
+        }
+        return self;
+    });
+
     function safe_to_flatten(value, compressor) {
         if (value instanceof AST_SymbolRef) {
             value = value.fixed_value();
@@ -9694,10 +9798,15 @@ merge(Compressor.prototype, {
                 prop = self.property = sub.property;
             }
         }
-        if (compressor.option("properties") && compressor.option("side_effects")
-            && prop instanceof AST_Number && expr instanceof AST_Array) {
+        var elements;
+        if (compressor.option("properties")
+            && compressor.option("side_effects")
+            && prop instanceof AST_Number
+            && expr instanceof AST_Array
+            && all(elements = expr.elements, function(value) {
+                return !(value instanceof AST_Spread);
+            })) {
             var index = prop.value;
-            var elements = expr.elements;
             var retValue = elements[index];
             if (safe_to_flatten(retValue, compressor)) {
                 var is_hole = retValue instanceof AST_Hole;
index 218372a..5830f2e 100644 (file)
@@ -702,6 +702,8 @@ function OutputStream(options) {
             || p instanceof AST_ObjectProperty
             // (1, {foo:2}).foo or (1, {foo:2})["foo"] ==> 2
             || p instanceof AST_PropAccess && p.expression === this
+            // ...(foo, bar, baz)
+            || p instanceof AST_Spread
             // !(foo, bar, baz)
             || p instanceof AST_Unary
             // var a = (1, 2), b = a + a; ==> b == 4
@@ -1231,6 +1233,10 @@ function OutputStream(options) {
         this.property.print(output);
         output.print("]");
     });
+    DEFPRINT(AST_Spread, function(output) {
+        output.print("...");
+        this.expression.print(output);
+    });
     DEFPRINT(AST_UnaryPrefix, function(output) {
         var op = this.operator;
         var exp = this.expression;
index a422a5a..872561f 100644 (file)
@@ -501,7 +501,16 @@ function tokenizer($TEXT, filename, html5_comments, shebang) {
 
     function handle_dot() {
         next();
-        return is_digit(peek().charCodeAt(0)) ? read_num(".") : token("punc", ".");
+        var ch = peek();
+        if (ch == ".") {
+            var op = ".";
+            do {
+                op += ".";
+                next();
+            } while (peek() == ".");
+            return token("operator", op);
+        }
+        return is_digit(ch.charCodeAt(0)) ? read_num(".") : token("punc", ".");
     }
 
     function read_word() {
@@ -1316,6 +1325,12 @@ function parse($TEXT, options) {
             if (allow_trailing_comma && is("punc", closing)) break;
             if (is("punc", ",") && allow_empty) {
                 a.push(new AST_Hole({ start: S.token, end: S.token }));
+            } else if (is("operator", "...") && parser === expression) {
+                a.push(new AST_Spread({
+                    start: S.token,
+                    expression: (next(), parser()),
+                    end: prev(),
+                }));
             } else {
                 a.push(parser());
             }
@@ -1343,6 +1358,15 @@ function parse($TEXT, options) {
             // allow trailing comma
             if (!options.strict && is("punc", "}")) break;
             var start = S.token;
+            if (is("operator", "...")) {
+                next();
+                a.push(new AST_Spread({
+                    start: start,
+                    expression: expression(false),
+                    end: prev(),
+                }));
+                continue;
+            }
             var key = as_property_key();
             if (is("punc", "(")) {
                 var func_start = S.token;
index 3831504..65e6cd6 100644 (file)
@@ -145,6 +145,9 @@ TreeTransformer.prototype = new TreeWalker;
         self.expression = self.expression.transform(tw);
         self.property = self.property.transform(tw);
     });
+    DEF(AST_Spread, function(self, tw) {
+        self.expression = self.expression.transform(tw);
+    });
     DEF(AST_Unary, function(self, tw) {
         self.expression = self.expression.transform(tw);
     });
diff --git a/test/compress/spread.js b/test/compress/spread.js
new file mode 100644 (file)
index 0000000..b7a6f75
--- /dev/null
@@ -0,0 +1,371 @@
+collapse_vars_1: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a;
+        [ ...a = "PASS", "PASS"].slice();
+        console.log(a);
+    }
+    expect: {
+        var a;
+        [ ...a = "PASS", "PASS"].slice();
+        console.log(a);
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+collapse_vars_2: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = "FAIL";
+        try {
+            a = "PASS";
+            [ ...42, "PASS"].slice();
+        } catch (e) {
+            console.log(a);
+        }
+    }
+    expect: {
+        var a = "FAIL";
+        try {
+            a = "PASS";
+            [ ...42, "PASS"].slice();
+        } catch (e) {
+            console.log(a);
+        }
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+collapse_vars_3: {
+    options = {
+        collapse_vars: true,
+    }
+    input: {
+        var a = "FAIL";
+        try {
+            [ ...(a = "PASS", 42), "PASS"].slice();
+        } catch (e) {
+            console.log(a);
+        }
+    }
+    expect: {
+        var a = "FAIL";
+        try {
+            [ ...(a = "PASS", 42), "PASS"].slice();
+        } catch (e) {
+            console.log(a);
+        }
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+collapse_vars_4: {
+    options = {
+        collapse_vars: true,
+        unused: true,
+    }
+    input: {
+        console.log(function(a) {
+            return a;
+        }(...[ "PASS", "FAIL" ]));
+    }
+    expect: {
+        console.log(function(a) {
+            return a;
+        }(...[ "PASS", "FAIL" ]));
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+dont_inline: {
+    options = {
+        inline: true,
+    }
+    input: {
+        console.log(function(a) {
+            return a;
+        }(...[ "PASS", "FAIL" ]));
+    }
+    expect: {
+        console.log(function(a) {
+            return a;
+        }(...[ "PASS", "FAIL" ]));
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+do_inline: {
+    options = {
+        properties: true,
+        inline: true,
+    }
+    input: {
+        console.log(function(a) {
+            return a;
+        }(...[ "PASS", "FAIL" ]));
+    }
+    expect: {
+        console.log(("FAIL", "PASS"));
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+drop_empty_call_1: {
+    options = {
+        side_effects: true,
+    }
+    input: {
+        try {
+            (function() {})(...null);
+        } catch (e) {
+            console.log("PASS");
+        }
+    }
+    expect: {
+        try {
+            [ ...null ];
+        } catch (e) {
+            console.log("PASS");
+        }
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+drop_empty_call_2: {
+    options = {
+        properties: true,
+        side_effects: true,
+    }
+    input: {
+        (function() {})(...[ console.log("PASS") ]);
+    }
+    expect: {
+        console.log("PASS");
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+convert_hole: {
+    options = {
+        properties: true,
+    }
+    input: {
+        console.log(...[ "PASS", , 42 ]);
+    }
+    expect: {
+        console.log("PASS", void 0, 42);
+    }
+    expect_stdout: "PASS undefined 42"
+    node_version: ">=6"
+}
+
+keep_property_access: {
+    options = {
+        properties: true,
+        side_effects: true,
+    }
+    input: {
+        console.log(function() {
+            return [ ..."foo" ][0];
+        }());
+    }
+    expect: {
+        console.log(function() {
+            return [ ..."foo" ][0];
+        }());
+    }
+    expect_stdout: "f"
+    node_version: ">=6"
+}
+
+keep_fargs: {
+    options = {
+        keep_fargs: "strict",
+        unused: true,
+    }
+    input: {
+        var a = [ "PASS" ];
+        (function(b, c) {
+            console.log(c);
+        })(console, ...a);
+    }
+    expect: {
+        var a = [ "PASS" ];
+        (function(b, c) {
+            console.log(c);
+        })(console, ...a);
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+reduce_vars_1: {
+    options = {
+        reduce_vars: true,
+        unused: true,
+    }
+    input: {
+        console.log(function(b, c) {
+            return c ? "PASS" : "FAIL";
+        }(..."foo"));
+    }
+    expect: {
+        console.log(function(b, c) {
+            return c ? "PASS" : "FAIL";
+        }(..."foo"));
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+reduce_vars_2: {
+    options = {
+        conditionals: true,
+        evaluate: true,
+        reduce_vars: true,
+    }
+    input: {
+        console.log(function(b, c) {
+            return c ? "PASS" : "FAIL";
+        }(..."foo"));
+    }
+    expect: {
+        console.log(function(b, c) {
+            return c ? "PASS" : "FAIL";
+        }(..."foo"));
+    }
+    expect_stdout: "PASS"
+    node_version: ">=6"
+}
+
+drop_object: {
+    options = {
+        side_effects: true,
+    }
+    input: {
+        ({ ...console.log("PASS") });
+    }
+    expect: {
+        console.log("PASS");
+    }
+    expect_stdout: "PASS"
+    node_version: ">=8"
+}
+
+keep_getter: {
+    options = {
+        side_effects: true,
+    }
+    input: {
+        ({
+            ...{
+                get p() {
+                    console.log("PASS");
+                },
+            },
+            get q() {
+                console.log("FAIL");
+            },
+        });
+    }
+    expect: {
+        ({
+            ...{
+                get p() {
+                    console.log("PASS");
+                },
+            },
+        });
+    }
+    expect_stdout: "PASS"
+    node_version: ">=8"
+}
+
+keep_accessor: {
+    options = {
+        properties: true,
+    }
+    input: {
+        var o = {
+            ...{
+                get p() {
+                    console.log("GET");
+                    return this.r;
+                },
+                set q(v) {
+                    console.log("SET", v);
+                },
+                r: 42,
+            },
+            r: null,
+        };
+        for (var k in o)
+            console.log(k, o[k]);
+    }
+    expect: {
+        var o = {
+            ...{
+                get p() {
+                    console.log("GET");
+                    return this.r;
+                },
+                set q(v) {
+                    console.log("SET", v);
+                },
+                r: 42,
+            },
+            r: null,
+        };
+        for (var k in o)
+            console.log(k, o[k]);
+    }
+    expect_stdout: [
+        "GET",
+        "p 42",
+        "q undefined",
+        "r null",
+    ]
+    node_version: ">=8"
+}
+
+unused_var_side_effects: {
+    options = {
+        unused: true,
+    }
+    input: {
+        (function f(a) {
+            var b = {
+                ...a,
+            };
+        })({
+            get p() {
+                console.log("PASS");
+            },
+        });
+    }
+    expect: {
+        (function(a) {
+            ({
+                ...a,
+            });
+        })({
+            get p() {
+                console.log("PASS");
+            },
+        });
+    }
+    expect_stdout: "PASS"
+    node_version: ">=8"
+}
index 2b1b42e..cda79f8 100644 (file)
@@ -135,7 +135,7 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
                 if (expr && !(expr instanceof U.AST_Hole)) {
                     node.start._permute++;
                     CHANGED = true;
-                    return expr;
+                    return expr instanceof U.AST_Spread ? expr.expression : expr;
                 }
             }
             else if (node instanceof U.AST_Binary) {
@@ -164,7 +164,7 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
                 ][ ((node.start._permute += step) * steps | 0) % 3 ];
                 if (expr) {
                     CHANGED = true;
-                    return expr;
+                    return expr instanceof U.AST_Spread ? expr.expression : expr;
                 }
                 if (node.expression instanceof U.AST_Function) {
                     // hoist and return expressions from the IIFE function expression
@@ -381,9 +381,8 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
             }
 
             if (in_list) {
-                // special case to drop object properties and switch branches
-                if (parent instanceof U.AST_Object
-                    || parent instanceof U.AST_Switch && parent.expression != node) {
+                // drop switch branches
+                if (parent instanceof U.AST_Switch && parent.expression != node) {
                     node.start._permute++;
                     CHANGED = true;
                     return List.skip;
@@ -404,7 +403,9 @@ module.exports = function reduce_test(testcase, minify_options, reduce_options)
                 }
 
                 // skip element/property from (destructured) array/object
-                if (parent instanceof U.AST_Array || parent instanceof U.AST_Destructured || parent instanceof AST_Object) {
+                if (parent instanceof U.AST_Array
+                    || parent instanceof U.AST_Destructured
+                    || parent instanceof U.AST_Object) {
                     node.start._permute++;
                     CHANGED = true;
                     return List.skip;
index 0e56bb1..7c76b34 100644 (file)
@@ -371,9 +371,24 @@ function createParams(noDuplicate) {
 }
 
 function createArgs(recurmax, stmtDepth, canThrow) {
+    recurmax--;
     var args = [];
-    for (var n = rng(4); --n >= 0;) {
-        args.push(rng(2) ? createValue() : createExpression(recurmax - 1, COMMA_OK, stmtDepth, canThrow));
+    for (var n = rng(4); --n >= 0;) switch (rng(50)) {
+      case 0:
+      case 1:
+        var name = getVarName();
+        if (canThrow && rng(8) === 0) {
+            args.push("..." + name);
+        } else {
+            args.push('...("" + ' + name + ")");
+        }
+        break;
+      case 2:
+        args.push("..." + createArrayLiteral(recurmax, stmtDepth, canThrow));
+        break;
+      default:
+        args.push(rng(2) ? createValue() : createExpression(recurmax, COMMA_OK, stmtDepth, canThrow));
+        break;
     }
     return args.join(", ");
 }
@@ -1044,13 +1059,30 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) {
 
 function createArrayLiteral(recurmax, stmtDepth, canThrow) {
     recurmax--;
-    var arr = "[";
-    for (var i = rng(6); --i >= 0;) {
+    var arr = [];
+    for (var i = rng(6); --i >= 0;) switch (rng(50)) {
+      case 0:
+      case 1:
         // in rare cases produce an array hole element
-        var element = rng(20) ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) : "";
-        arr += element + ", ";
+        arr.push("");
+        break;
+      case 2:
+      case 3:
+        var name = getVarName();
+        if (canThrow && rng(8) === 0) {
+            arr.push("..." + name);
+        } else {
+            arr.push('...("" + ' + name + ")");
+        }
+        break;
+      case 4:
+        arr.push("..." + createArrayLiteral(recurmax, stmtDepth, canThrow));
+        break;
+      default:
+        arr.push(createExpression(recurmax, COMMA_OK, stmtDepth, canThrow));
+        break;
     }
-    return arr + "]";
+    return "[" + arr.join(", ") + "]";
 }
 
 var SAFE_KEYS = [
@@ -1135,13 +1167,20 @@ function createObjectFunction(recurmax, stmtDepth, canThrow) {
 function createObjectLiteral(recurmax, stmtDepth, canThrow) {
     recurmax--;
     var obj = ["({"];
-    for (var i = rng(6); --i >= 0;) switch (rng(30)) {
+    for (var i = rng(6); --i >= 0;) switch (rng(50)) {
       case 0:
-        obj.push(createObjectFunction(recurmax, stmtDepth, canThrow));
-        break;
       case 1:
         obj.push(getVarName() + ",");
         break;
+      case 2:
+        obj.push(createObjectFunction(recurmax, stmtDepth, canThrow));
+        break;
+      case 3:
+        obj.push("..." + getVarName() + ",");
+        break;
+      case 4:
+        obj.push("..." + createObjectLiteral(recurmax, stmtDepth, canThrow) + ",");
+        break;
       default:
         obj.push(createObjectKey(recurmax, stmtDepth, canThrow) + ":(" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "),");
         break;
@@ -1591,6 +1630,9 @@ function patch_try_catch(orig, toplevel) {
             } else if (result.name == "TypeError" && /'in'/.test(result.message)) {
                 index = result.ufuzz_catch;
                 return orig.slice(0, index) + result.ufuzz_var + ' = new Error("invalid `in`");' + orig.slice(index);
+            } else if (result.name == "TypeError" && /not iterable/.test(result.message)) {
+                index = result.ufuzz_catch;
+                return orig.slice(0, index) + result.ufuzz_var + ' = new Error("spread not iterable");' + orig.slice(index);
             } else if (result.name == "RangeError" && result.message == "Maximum call stack size exceeded") {
                 index = result.ufuzz_try;
                 return orig.slice(0, index) + 'throw new Error("skipping infinite recursion");' + orig.slice(index);
@@ -1656,6 +1698,7 @@ for (var round = 1; round <= num_iterations; round++) {
                 ok = sandbox.same_stdout(orig_result[toplevel ? 3 : 2], uglify_result);
             }
             // ignore difference in error message caused by `in`
+            // ignore difference in error message caused by spread syntax
             // ignore difference in depth of termination caused by infinite recursion
             if (!ok) {
                 var orig_skipped = patch_try_catch(original_code, toplevel);