From 1e4985ed9e0822118ad01313af09008af1e9f036 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 5 Dec 2020 21:19:31 +0000 Subject: [PATCH] support spread syntax (#4328) --- lib/ast.js | 32 +++- lib/compress.js | 177 +++++++++++++++---- lib/output.js | 6 + lib/parse.js | 26 ++- lib/transform.js | 3 + test/compress/spread.js | 371 ++++++++++++++++++++++++++++++++++++++++ test/reduce.js | 13 +- test/ufuzz/index.js | 63 +++++-- 8 files changed, 635 insertions(+), 56 deletions(-) create mode 100644 test/compress/spread.js diff --git a/lib/ast.js b/lib/ast.js index 2a4631f7..792264ff 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -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"); + } }); }, }); diff --git a/lib/compress.js b/lib/compress.js index 7434a20d..a78cf6ef 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -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; diff --git a/lib/output.js b/lib/output.js index 218372a8..5830f2e7 100644 --- a/lib/output.js +++ b/lib/output.js @@ -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; diff --git a/lib/parse.js b/lib/parse.js index a422a5a6..872561ff 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -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; diff --git a/lib/transform.js b/lib/transform.js index 3831504d..65e6cd60 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -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 index 00000000..b7a6f753 --- /dev/null +++ b/test/compress/spread.js @@ -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" +} diff --git a/test/reduce.js b/test/reduce.js index 2b1b42e7..cda79f8a 100644 --- a/test/reduce.js +++ b/test/reduce.js @@ -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; diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index 0e56bb1d..7c76b348 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -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); -- 2.34.1