diff --git a/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java b/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java
index 105b35d0063..dd5fb8ad3c1 100644
--- a/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java
+++ b/src/com/google/javascript/jscomp/EarlyEs6ToEs3Converter.java
@@ -16,6 +16,7 @@
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.NodeTraversal.Callback;
@@ -237,6 +238,7 @@ private void visitRestParam(NodeTraversal t, Node restParam, Node paramList) {
name.setJSDocInfo(builder.build());
}
+ // TODO(b/74074478): Use a general utility method instead of an inlined loop.
Node newArr = IR.var(IR.name(REST_PARAMS), IR.arraylit());
functionBody.addChildToFront(newArr.useSourceInfoIfMissingFromForTree(restParam));
Node init = IR.var(IR.name(REST_INDEX), IR.number(restIndex));
@@ -282,64 +284,217 @@ public void visit(Node n) {
* new Function.prototype.bind.apply(F, [].concat($jscomp.arrayFromIterable(args)))
*/
private void visitArrayLitOrCallWithSpread(Node node, Node parent) {
- checkArgument(node.isCall() || node.isArrayLit() || node.isNew());
+ if (node.isArrayLit()) {
+ visitArrayLitWithSpread(node, parent);
+ } else if (node.isCall()) {
+ visitCallWithSpread(node, parent);
+ } else {
+ checkArgument(node.isNew(), node);
+ visitNewWithSpread(node, parent);
+ }
+ }
+
+ /**
+ * Extracts child nodes from an array literal, call or new node that may contain spread operators
+ * into a list of nodes that may be concatenated with Array.concat() to get an array.
+ *
+ *
Example: [a, b, ...x, c, ...arguments] returns a list containing [ [a, b],
+ * $jscomp.arrayFromIterable(x), [c], $jscomp.arrayFromIterable(arguments) ]
+ *
+ *
IMPORTANT: Call and New nodes must have the first, callee, child removed already.
+ *
+ *
Note that all elements of the returned list will be one of:
+ *
+ *
+ * - array literal
+ *
- $jscomp.arrayFromIterable(spreadExpression)
+ *
+ *
+ * TODO(bradfordcsmith): When this pass moves after type checking, we can use type information to
+ * avoid unnecessary calls to $jscomp.arrayFromIterable().
+ */
+ private List extractSpreadGroups(Node parentNode) {
+ checkArgument(parentNode.isCall() || parentNode.isArrayLit() || parentNode.isNew());
List groups = new ArrayList<>();
Node currGroup = null;
- Node callee = node.isArrayLit() ? null : node.removeFirstChild();
- Node currElement = node.removeFirstChild();
+ Node currElement = parentNode.removeFirstChild();
while (currElement != null) {
if (currElement.isSpread()) {
- if (currGroup != null) {
- groups.add(currGroup);
- currGroup = null;
+ Node spreadExpression = currElement.removeFirstChild();
+ if (spreadExpression.isArrayLit()) {
+ // We can expand an array literal spread in place.
+ if (currGroup == null) {
+ // [...[spread, contents], a, b]
+ // we can use this array lit itself as a group and append following elements to it
+ currGroup = spreadExpression;
+ } else {
+ // [ a, b, ...[spread, contents], c]
+ // Just add contents of this array lit to the group we were already collecting.
+ currGroup.addChildrenToBack(spreadExpression.removeChildren());
+ }
+ } else {
+ // We need to treat the spread expression as a separate group
+ if (currGroup != null) {
+ // finish off and add the group we were collecting before
+ groups.add(currGroup);
+ currGroup = null;
+ }
+
+ groups.add(Es6ToEs3Util.arrayFromIterable(compiler, spreadExpression));
}
- groups.add(Es6ToEs3Util.arrayFromIterable(compiler, currElement.removeFirstChild()));
} else {
if (currGroup == null) {
currGroup = IR.arraylit();
}
currGroup.addChildToBack(currElement);
}
- currElement = node.removeFirstChild();
+ currElement = parentNode.removeFirstChild();
}
if (currGroup != null) {
groups.add(currGroup);
}
- Node result = null;
- Node firstGroup = node.isNew() ? IR.arraylit(IR.nullNode()) : IR.arraylit();
+ return groups;
+ }
+
+ /**
+ * Processes array literals containing spreads.
+ *
+ * Example:
+ *
+ *
+ * [1, 2, ...x, 4, 5] => [1, 2].concat($jscomp.arrayFromIterable(x), [4, 5])
+ *
+ */
+ private void visitArrayLitWithSpread(Node node, Node parent) {
+ checkArgument(node.isArrayLit());
+ List groups = extractSpreadGroups(node);
+ Node baseArrayLit;
+ if (groups.get(0).isArrayLit()) {
+ baseArrayLit = groups.remove(0);
+ } else {
+ baseArrayLit = IR.arraylit();
+ // [].concat(g0, g1, g2, ..., gn)
+ }
Node joinedGroups =
- IR.call(IR.getprop(firstGroup, IR.string("concat")), groups.toArray(new Node[0]));
- if (node.isArrayLit()) {
- result = joinedGroups;
- } else if (node.isCall()) {
- if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) {
- Node statement = node;
- while (!NodeUtil.isStatement(statement)) {
- statement = statement.getParent();
- }
- Node freshVar = IR.name(FRESH_SPREAD_VAR + compiler.getUniqueNameIdSupplier().get());
- Node n = IR.var(freshVar.cloneTree());
- n.useSourceInfoIfMissingFromForTree(statement);
- statement.getParent().addChildBefore(n, statement);
- callee.addChildToFront(IR.assign(freshVar.cloneTree(), callee.removeFirstChild()));
- result = IR.call(
- IR.getprop(callee, IR.string("apply")),
- freshVar,
- joinedGroups);
+ groups.isEmpty()
+ ? baseArrayLit
+ : IR.call(IR.getprop(baseArrayLit, IR.string("concat")), groups.toArray(new Node[0]));
+ joinedGroups.useSourceInfoIfMissingFromForTree(node);
+ parent.replaceChild(node, joinedGroups);
+ compiler.reportChangeToEnclosingScope(joinedGroups);
+ }
+
+ /**
+ * Processes calls containing spreads.
+ *
+ * Examples:
+ *
+ *
+ * f(...arr) => f.apply(null, $jscomp.arrayFromIterable(arr))
+ * f(a, ...arr) => f.apply(null, [a].concat($jscomp.arrayFromIterable(arr)))
+ * f(...arr, b) => f.apply(null, [].concat($jscomp.arrayFromIterable(arr), [b]))
+ *
+ */
+ private void visitCallWithSpread(Node node, Node parent) {
+ checkArgument(node.isCall());
+ // must remove callee before extracting argument groups
+ Node callee = node.removeFirstChild();
+ Node joinedGroups;
+ if (node.hasOneChild() && isSpreadOfArguments(node.getOnlyChild())) {
+ // Check for special case of
+ // `foo(...arguments)` and pass `arguments` directly to `foo.apply(null, arguments)`.
+ // We want to avoid calling $jscomp.arrayFromIterable(arguments) for this case,
+ // because it can have side effects, which prevents code removal.
+ // TODO(b/74074478): generalize this to avoid ever calling $jscomp.arrayFromIterable() for
+ // `arguments`.
+ joinedGroups = node.removeFirstChild().removeFirstChild();
+ } else {
+ List groups = extractSpreadGroups(node);
+ checkState(!groups.isEmpty());
+ if (groups.size() == 1) {
+ // single group can just be passed to apply() as-is
+ // It could be `arguments`, an array literal, or $jscomp.arrayFromIterable(someExpression).
+ joinedGroups = groups.remove(0);
} else {
- Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode();
- result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups);
+ // If the first group is an array literal, we can just use that for concatenation,
+ // otherwise use an empty array literal.
+ Node baseArrayLit = groups.get(0).isArrayLit() ? groups.remove(0) : IR.arraylit();
+ joinedGroups =
+ groups.isEmpty()
+ ? baseArrayLit
+ : IR.call(
+ IR.getprop(baseArrayLit, IR.string("concat")), groups.toArray(new Node[0]));
}
- } else {
- if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) {
- // TODO(tbreisacher): Support this in ES3 too by not relying on Function.bind.
- Es6ToEs3Util.cannotConvert(
- compiler, node, "\"...\" passed to a constructor (consider using --language_out=ES5)");
+ }
+
+ Node result = null;
+ if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) {
+ // foo().method(...[a, b, c])
+ // must convert to
+ // var freshVar;
+ // (freshVar = foo()).method.apply(freshVar, [a, b, c])
+ Node statement = node;
+ while (!NodeUtil.isStatement(statement)) {
+ statement = statement.getParent();
}
- Node bindApply = NodeUtil.newQName(compiler,
- "Function.prototype.bind.apply");
- result = IR.newNode(IR.call(bindApply, callee, joinedGroups));
+ Node freshVar = IR.name(FRESH_SPREAD_VAR + compiler.getUniqueNameIdSupplier().get());
+ Node n = IR.var(freshVar.cloneTree());
+ n.useSourceInfoIfMissingFromForTree(statement);
+ statement.getParent().addChildBefore(n, statement);
+ callee.addChildToFront(IR.assign(freshVar.cloneTree(), callee.removeFirstChild()));
+ result = IR.call(IR.getprop(callee, IR.string("apply")), freshVar, joinedGroups);
+ } else {
+ // foo.method(...[a, b, c]) -> foo.method.apply(foo, [a, b, c]
+ // or
+ // foo(...[a, b, c]) -> foo.apply(null, [a, b, c])
+ Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode();
+ result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups);
+ }
+ result.useSourceInfoIfMissingFromForTree(node);
+ parent.replaceChild(node, result);
+ compiler.reportChangeToEnclosingScope(result);
+ }
+
+ private boolean isSpreadOfArguments(Node n) {
+ return n.isSpread() && n.getOnlyChild().matchesQualifiedName("arguments");
+ }
+
+ /**
+ * Processes new calls containing spreads.
+ *
+ * Example:
+ *
+ *
+ * new F(...args) =>
+ * new Function.prototype.bind.apply(F, [].concat($jscomp.arrayFromIterable(args)))
+ *
+ */
+ private void visitNewWithSpread(Node node, Node parent) {
+ checkArgument(node.isNew());
+ // must remove callee before extracting argument groups
+ Node callee = node.removeFirstChild();
+ List groups = extractSpreadGroups(node);
+ // We need to generate
+ // new (Function.prototype.bind.apply(callee, [null].concat(other, args))();
+ // null stands in for the 'this' arg to bind
+ Node baseArrayLit;
+ if (groups.get(0).isArrayLit()) {
+ baseArrayLit = groups.remove(0);
+ } else {
+ baseArrayLit = IR.arraylit();
+ }
+ baseArrayLit.addChildToFront(IR.nullNode());
+ Node joinedGroups =
+ groups.isEmpty()
+ ? baseArrayLit
+ : IR.call(IR.getprop(baseArrayLit, IR.string("concat")), groups.toArray(new Node[0]));
+ if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) {
+ // TODO(tbreisacher): Support this in ES3 too by not relying on Function.bind.
+ Es6ToEs3Util.cannotConvert(
+ compiler, node, "\"...\" passed to a constructor (consider using --language_out=ES5)");
}
+ Node bindApply = NodeUtil.newQName(compiler, "Function.prototype.bind.apply");
+ Node result = IR.newNode(IR.call(bindApply, callee, joinedGroups));
result.useSourceInfoIfMissingFromForTree(node);
parent.replaceChild(node, result);
compiler.reportChangeToEnclosingScope(result);
diff --git a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
index 75a5d9a1040..2264ece2e89 100644
--- a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
+++ b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
@@ -150,6 +150,46 @@ protected int getNumRepetitions() {
return 1;
}
+ public void testSpreadOfArrayLiteral() {
+ test("[...[1, 2], 3, ...[4, 5], 6, ...[7, 8]]", "[1, 2, 3, 4, 5, 6, 7, 8]");
+ test(
+ "function use() {} use(...[1, 2], 3, ...[4, 5], 6, ...[7, 8]);",
+ "function use() {} use.apply(null, [1, 2, 3, 4, 5, 6, 7, 8])");
+ }
+
+ public void testExplicitSuperSpread() {
+ test(
+ lines(
+ "class A {",
+ " constructor(a) {",
+ " this.p = a;",
+ " }",
+ "}",
+ "",
+ "class B extends A {",
+ " constructor(a) {",
+ " super(...arguments);",
+ " }",
+ "}",
+ ""),
+ lines(
+ "/** @struct @constructor */",
+ "var A = function(a) {",
+ " this.p=a",
+ "};",
+ "",
+ "/**",
+ " * @struct",
+ " * @constructor",
+ " * @extends {A}",
+ " */",
+ "var B = function(a){",
+ " A.apply(this, arguments)",
+ "};",
+ "$jscomp.inherits(B,A)",
+ ""));
+ }
+
public void testObjectLiteralStringKeysWithNoValue() {
test("var x = {a, b};", "var x = {a: a, b: b};");
assertThat(getLastCompiler().injected).isEmpty();
@@ -1471,7 +1511,7 @@ public void testSuperSpread() {
"var D = function(){};",
"/** @constructor @struct @extends {D} */",
"var C=function(args) {",
- " D.apply(this, [].concat($jscomp.arrayFromIterable(args)));",
+ " D.apply(this, $jscomp.arrayFromIterable(args));",
"};",
"$jscomp.inherits(C,D);"));
assertThat(getLastCompiler().injected)
@@ -2397,19 +2437,21 @@ public void testForOfJSDoc() {
}
public void testSpreadArray() {
- test("var arr = [1, 2, ...mid, 4, 5];",
- "var arr = [].concat([1, 2], $jscomp.arrayFromIterable(mid), [4, 5]);");
+ test(
+ "var arr = [1, 2, ...mid, 4, 5];",
+ "var arr = [1, 2].concat($jscomp.arrayFromIterable(mid), [4, 5]);");
assertThat(getLastCompiler().injected).containsExactly("es6/util/arrayfromiterable");
- test("var arr = [1, 2, ...mid(), 4, 5];",
- "var arr = [].concat([1, 2], $jscomp.arrayFromIterable(mid()), [4, 5]);");
- test("var arr = [1, 2, ...mid, ...mid2(), 4, 5];",
- "var arr = [].concat([1, 2], $jscomp.arrayFromIterable(mid),"
- + " $jscomp.arrayFromIterable(mid2()), [4, 5]);");
- test("var arr = [...mid()];",
- "var arr = [].concat($jscomp.arrayFromIterable(mid()));");
- test("f(1, [2, ...mid, 4], 5);",
- "f(1, [].concat([2], $jscomp.arrayFromIterable(mid), [4]), 5);");
+ test(
+ "var arr = [1, 2, ...mid(), 4, 5];",
+ "var arr = [1, 2].concat($jscomp.arrayFromIterable(mid()), [4, 5]);");
+ test(
+ "var arr = [1, 2, ...mid, ...mid2(), 4, 5];",
+ lines(
+ "var arr = [1,2].concat(",
+ " $jscomp.arrayFromIterable(mid), $jscomp.arrayFromIterable(mid2()), [4, 5]);"));
+ test("var arr = [...mid()];", "var arr = [].concat($jscomp.arrayFromIterable(mid()));");
+ test("f(1, [2, ...mid, 4], 5);", "f(1, [2].concat($jscomp.arrayFromIterable(mid), [4]), 5);");
test(
"function f() { return [...arguments]; };",
lines(
@@ -2458,39 +2500,47 @@ public void testForOfOnNonIterable() {
}
public void testSpreadCall() {
- test("f(...arr);", "f.apply(null, [].concat($jscomp.arrayFromIterable(arr)));");
- test("f(0, ...g());", "f.apply(null, [].concat([0], $jscomp.arrayFromIterable(g())));");
+ test("f(...arr);", "f.apply(null, $jscomp.arrayFromIterable(arr));");
+ test("f(0, ...g());", "f.apply(null, [0].concat($jscomp.arrayFromIterable(g())));");
test("f(...arr, 1);", "f.apply(null, [].concat($jscomp.arrayFromIterable(arr), [1]));");
- test("f(0, ...g(), 2);", "f.apply(null, [].concat([0], $jscomp.arrayFromIterable(g()), [2]));");
- test("obj.m(...arr);", "obj.m.apply(obj, [].concat($jscomp.arrayFromIterable(arr)));");
- test("x.y.z.m(...arr);", "x.y.z.m.apply(x.y.z, [].concat($jscomp.arrayFromIterable(arr)));");
- test("f(a, ...b, c, ...d, e);",
- "f.apply(null, [].concat([a], $jscomp.arrayFromIterable(b),"
- + " [c], $jscomp.arrayFromIterable(d), [e]));");
-
- test("Factory.create().m(...arr);",
- lines(
- "var $jscomp$spread$args0;",
- "($jscomp$spread$args0 = Factory.create()).m.apply("
- + "$jscomp$spread$args0, [].concat($jscomp.arrayFromIterable(arr)));"
- ));
+ test("f(0, ...g(), 2);", "f.apply(null, [0].concat($jscomp.arrayFromIterable(g()), [2]));");
+ test("obj.m(...arr);", "obj.m.apply(obj, $jscomp.arrayFromIterable(arr));");
+ test("x.y.z.m(...arr);", "x.y.z.m.apply(x.y.z, $jscomp.arrayFromIterable(arr));");
+ test(
+ "f(a, ...b, c, ...d, e);",
+ lines(
+ "f.apply(",
+ " null,",
+ " [a].concat(",
+ " $jscomp.arrayFromIterable(b),",
+ " [c],",
+ " $jscomp.arrayFromIterable(d),",
+ " [e]));"));
+
+ test(
+ "Factory.create().m(...arr);",
+ lines(
+ "var $jscomp$spread$args0;",
+ "($jscomp$spread$args0 = Factory.create()).m.apply(",
+ " $jscomp$spread$args0, $jscomp.arrayFromIterable(arr));"));
- test("var x = b ? Factory.create().m(...arr) : null;",
+ test(
+ "var x = b ? Factory.create().m(...arr) : null;",
lines(
"var $jscomp$spread$args0;",
"var x = b ? ($jscomp$spread$args0 = Factory.create()).m.apply($jscomp$spread$args0, ",
- " [].concat($jscomp.arrayFromIterable(arr))) : null;"));
+ " $jscomp.arrayFromIterable(arr)) : null;"));
- test("getF()(...args);", "getF().apply(null, [].concat($jscomp.arrayFromIterable(args)));");
+ test("getF()(...args);", "getF().apply(null, $jscomp.arrayFromIterable(args));");
test(
"F.c().m(...a); G.d().n(...b);",
lines(
"var $jscomp$spread$args0;",
"($jscomp$spread$args0 = F.c()).m.apply($jscomp$spread$args0,",
- " [].concat($jscomp.arrayFromIterable(a)));",
+ " $jscomp.arrayFromIterable(a));",
"var $jscomp$spread$args1;",
"($jscomp$spread$args1 = G.d()).n.apply($jscomp$spread$args1,",
- " [].concat($jscomp.arrayFromIterable(b)));"));
+ " $jscomp.arrayFromIterable(b));"));
this.mode = TypeInferenceMode.BOTH;
@@ -2526,7 +2576,7 @@ public void testSpreadCall() {
"var arr = [1,2]",
"var $jscomp$spread$args0;",
"($jscomp$spread$args0 = Factory.create()).m.apply(",
- " $jscomp$spread$args0, [].concat($jscomp.arrayFromIterable(arr)));"));
+ " $jscomp$spread$args0, $jscomp.arrayFromIterable(arr));"));
}
public void testSpreadNew() {
diff --git a/test/com/google/javascript/jscomp/IntegrationTest.java b/test/com/google/javascript/jscomp/IntegrationTest.java
index be43ea9fbc8..20bf5d67c5f 100644
--- a/test/com/google/javascript/jscomp/IntegrationTest.java
+++ b/test/com/google/javascript/jscomp/IntegrationTest.java
@@ -3312,7 +3312,7 @@ public void testIssue724() {
" getType.toString.apply(functionToCheck) === '[object Function]';",
"};");
String result =
- "isFunction = function(a){ var b={}; return a && '[object Function]' === b.b.a(a); }";
+ "isFunction = function(a){ var b={}; return a && '[object Function]' === b.a.apply(a); }";
test(options, code, result);
}
@@ -4875,6 +4875,40 @@ public void testPureFunctionIdentifierWorksWithMultipleNames() {
"window._customData.foo='bar';");
}
+ public void testSpreadArgumentsPassedToSuperDoesNotPreventRemoval() {
+ CompilerOptions options = createCompilerOptions();
+ options.setLanguageIn(LanguageMode.ECMASCRIPT_2017);
+ options.setLanguageOut(LanguageMode.ECMASCRIPT5);
+ CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
+
+ // Create a noninjecting compiler avoid comparing all the polyfill code.
+ useNoninjectingCompiler = true;
+
+ // include externs definitions for the stuff that would have been injected
+ ImmutableList.Builder externsList = ImmutableList.builder();
+ externsList.addAll(externs);
+ externsList.add(SourceFile.fromCode("extraExterns", "var $jscomp = {};"));
+ externs = externsList.build();
+
+ test(
+ options,
+ lines(
+ "", // preserve newlines
+ "class A {",
+ " constructor(a, b) {",
+ " this.a = a;",
+ " this.b = b;",
+ " }",
+ "}",
+ "class B extends A {",
+ " constructor() {",
+ " super(...arguments);",
+ " }",
+ "}",
+ "var b = new B();"),
+ "");
+ }
+
public void testUnnecessaryBackslashInStringLiteral() {
CompilerOptions options = createCompilerOptions();
test(options,
diff --git a/test/com/google/javascript/jscomp/IntegrationTestCase.java b/test/com/google/javascript/jscomp/IntegrationTestCase.java
index eba76e9a6e3..307f2b38d72 100644
--- a/test/com/google/javascript/jscomp/IntegrationTestCase.java
+++ b/test/com/google/javascript/jscomp/IntegrationTestCase.java
@@ -201,6 +201,11 @@ abstract class IntegrationTestCase extends TestCase {
" * @constructor",
" */",
"function Function(var_args) {}",
+ "/**",
+ " * @param {*} context",
+ " * @param {!IArrayLike} args",
+ " */",
+ "Function.prototype.apply = function (context, args) {};",
"/** @param {...*} var_args */",
"Function.prototype.call = function (var_args) {};",
"",