com.google.javascript.jscomp.Es6RewriteRestAndSpread Maven / Gradle / Ivy
Show all versions of closure-compiler-linter Show documentation
/*
* Copyright 2014 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import java.util.ArrayList;
import java.util.List;
/** Converts REST parameters and SPREAD expressions. */
public final class Es6RewriteRestAndSpread extends NodeTraversal.AbstractPostOrderCallback
implements HotSwapCompilerPass {
static final DiagnosticType BAD_REST_PARAMETER_ANNOTATION =
DiagnosticType.warning(
"BAD_REST_PARAMETER_ANNOTATION",
"Missing \"...\" in type annotation for rest parameter.");
// The name of the index variable for populating the rest parameter array.
private static final String REST_INDEX = "$jscomp$restIndex";
// The name of the placeholder for the rest parameters.
private static final String REST_PARAMS = "$jscomp$restParams";
private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args";
private static final FeatureSet transpiledFeatures =
FeatureSet.BARE_MINIMUM.with(Feature.REST_PARAMETERS, Feature.SPREAD_EXPRESSIONS);
private final AbstractCompiler compiler;
private final JSType arrayType;
private final JSType boolType;
private final JSType concatFnType;
private final JSType nullType;
private final JSType numberType;
private final JSType functionFunctionType;
public Es6RewriteRestAndSpread(AbstractCompiler compiler) {
this.compiler = compiler;
if (compiler.hasTypeCheckingRun()) {
JSTypeRegistry registry = compiler.getTypeRegistry();
this.arrayType = registry.getNativeType(JSTypeNative.ARRAY_TYPE);
this.boolType = registry.getNativeType(JSTypeNative.BOOLEAN_TYPE);
this.concatFnType = arrayType.findPropertyType("concat");
this.nullType = registry.getNativeType(JSTypeNative.NULL_TYPE);
this.numberType = registry.getNativeType(JSTypeNative.NUMBER_TYPE);
this.functionFunctionType = registry.getNativeType(JSTypeNative.FUNCTION_FUNCTION_TYPE);
} else {
this.arrayType = null;
this.boolType = null;
this.concatFnType = null;
this.nullType = null;
this.numberType = null;
this.functionFunctionType = null;
}
}
@Override
public void process(Node externs, Node root) {
TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, this);
TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, this);
TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures);
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, transpiledFeatures, this);
TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures);
}
@Override
public void visit(NodeTraversal traversal, Node current, Node parent) {
switch (current.getToken()) {
case ITER_REST:
visitRestParam(traversal, current, parent);
break;
case ARRAYLIT:
case NEW:
case CALL:
for (Node child : current.children()) {
if (child.isSpread()) {
visitArrayLitOrCallWithSpread(current);
break;
}
}
break;
default:
break;
}
}
/** Processes a rest parameter */
private void visitRestParam(NodeTraversal t, Node restParam, Node paramList) {
Node functionBody = paramList.getNext();
int restIndex = paramList.getIndexOfChild(restParam);
Node nameNode = restParam.getOnlyChild();
String paramName = nameNode.getString();
// Swap the existing param into the list, moving requisite AST annotations.
nameNode.setVarArgs(true);
nameNode.setJSDocInfo(restParam.getJSDocInfo());
paramList.replaceChild(restParam, nameNode.detach());
// Make sure rest parameters are typechecked.
JSDocInfo inlineInfo = restParam.getJSDocInfo();
JSDocInfo functionInfo = NodeUtil.getBestJSDocInfo(paramList.getParent());
final JSTypeExpression paramTypeAnnotation;
if (inlineInfo != null) {
paramTypeAnnotation = inlineInfo.getType();
} else if (functionInfo != null) {
paramTypeAnnotation = functionInfo.getParameterType(paramName);
} else {
paramTypeAnnotation = null;
}
// TODO(lharker): we should report this error in typechecking, not during transpilation, so
// that it also occurs when natively typechecking ES6.
if (paramTypeAnnotation != null
&& paramTypeAnnotation.getRoot().getToken() != Token.ITER_REST) {
compiler.report(JSError.make(restParam, BAD_REST_PARAMETER_ANNOTATION));
}
if (!functionBody.hasChildren()) {
// If function has no body, we are done!
t.reportCodeChange();
return;
}
// Don't insert these directly, just clone them.
Node newArrayName = IR.name(REST_PARAMS).setJSType(arrayType);
Node cursorName = IR.name(REST_INDEX).setJSType(numberType);
Node newBlock = IR.block().useSourceInfoFrom(functionBody);
Node name = IR.name(paramName);
Node let = IR.let(name, newArrayName).useSourceInfoIfMissingFromForTree(functionBody);
newBlock.addChildToFront(let);
NodeUtil.addFeatureToScript(t.getCurrentScript(), Feature.LET_DECLARATIONS, compiler);
for (Node child : functionBody.children()) {
newBlock.addChildToBack(child.detach());
}
Node newArrayDeclaration = IR.var(newArrayName.cloneTree(), arrayLitWithJSType());
functionBody.addChildToFront(newArrayDeclaration.useSourceInfoIfMissingFromForTree(restParam));
// TODO(b/74074478): Use a general utility method instead of an inlined loop.
Node copyLoop =
IR.forNode(
IR.var(cursorName.cloneTree(), IR.number(restIndex).setJSType(numberType)),
IR.lt(
cursorName.cloneTree(),
IR.getprop(IR.name("arguments"), IR.string("length")).setJSType(numberType))
.setJSType(boolType),
IR.inc(cursorName.cloneTree(), false).setJSType(numberType),
IR.block(
IR.exprResult(
IR.assign(
IR.getelem(
newArrayName.cloneTree(),
IR.sub(
cursorName.cloneTree(),
IR.number(restIndex).setJSType(numberType))
.setJSType(numberType)),
IR.getelem(IR.name("arguments"), cursorName.cloneTree())
.setJSType(numberType))
.setJSType(numberType))))
.useSourceInfoIfMissingFromForTree(restParam);
functionBody.addChildAfter(copyLoop, newArrayDeclaration);
functionBody.addChildToBack(newBlock);
compiler.reportChangeToEnclosingScope(newBlock);
// For now, we are running transpilation before type-checking, so we'll
// need to make sure changes don't invalidate the JSDoc annotations.
// Therefore we keep the parameter list the same length and only initialize
// the values if they are set to undefined.
// TODO(lharker): the above comment is out of date since we move transpilation after
// typechecking. see if we can improve transpilation and not keep the parameter list the
// same length?
}
/**
* Processes array literals or calls to eliminate spreads.
*
* Examples:
*
*
* - [1, 2, ...x, 4, 5] => [].concat([1, 2], $jscomp.arrayFromIterable(x), [4, 5])
*
- f(1, ...arr) => f.apply(null, [1].concat($jscomp.arrayFromIterable(arr)))
*
- new F(...args) => new Function.prototype.bind.apply(F,
* [null].concat($jscomp.arrayFromIterable(args)))
*
*/
private void visitArrayLitOrCallWithSpread(Node spreadParent) {
if (spreadParent.isArrayLit()) {
visitArrayLitContainingSpread(spreadParent);
} else if (spreadParent.isCall()) {
visitCallContainingSpread(spreadParent);
} else {
checkArgument(spreadParent.isNew(), spreadParent);
visitNewWithSpread(spreadParent);
}
}
/**
* Extracts child nodes from an ARRAYLIT, 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().
*
* TODO(nickreid): Stop mutating `spreadParent`.
*/
private List extractSpreadGroups(Node spreadParent) {
checkArgument(spreadParent.isCall() || spreadParent.isArrayLit() || spreadParent.isNew());
List groups = new ArrayList<>();
Node currGroup = null;
for (Node currElement = spreadParent.removeFirstChild();
currElement != null;
currElement = spreadParent.removeFirstChild()) {
if (currElement.isSpread()) {
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));
}
} else {
if (currGroup == null) {
currGroup = arrayLitWithJSType();
}
currGroup.addChildToBack(currElement);
}
}
if (currGroup != null) {
groups.add(currGroup);
}
return groups;
}
/**
* Processes array literals containing spreads.
*
* Example:
*
*
* [1, 2, ...x, 4, 5] => [1, 2].concat($jscomp.arrayFromIterable(x), [4, 5])
*
*/
private void visitArrayLitContainingSpread(Node spreadParent) {
checkArgument(spreadParent.isArrayLit());
List groups = extractSpreadGroups(spreadParent);
final Node baseArrayLit;
if (groups.get(0).isArrayLit()) {
// g0.concat(g1, g2, ..., gn)
baseArrayLit = groups.remove(0);
} else {
// [].concat(g0, g1, g2, ..., gn)
baseArrayLit = arrayLitWithJSType();
}
final Node joinedGroups;
if (groups.isEmpty()) {
joinedGroups = baseArrayLit;
} else {
Node concat = IR.getprop(baseArrayLit, IR.string("concat")).setJSType(concatFnType);
joinedGroups = IR.call(concat, groups.toArray(new Node[0]));
}
joinedGroups.useSourceInfoIfMissingFromForTree(spreadParent);
joinedGroups.setJSType(arrayType);
spreadParent.replaceWith(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 visitCallContainingSpread(Node spreadParent) {
checkArgument(spreadParent.isCall());
Node callee = spreadParent.getFirstChild();
// ES6 classes must all be transpiled away before this pass runs.
checkState(!callee.isSuper(), "Cannot spread into super calls");
// Check if the callee has side effects before removing it from the AST (since some NodeUtil
// methods assume the node they are passed has a non-null parent).
boolean calleeMayHaveSideEffects = compiler.getAstAnalyzer().mayHaveSideEffects(callee);
// Must remove callee before extracting argument groups.
spreadParent.removeChild(callee);
while (callee.isCast()) {
// Drop any CAST nodes. They're not needed anymore since this pass runs at the end of
// the checks phase, and they complicate detecting GETPROP/GETELEM callees.
callee = callee.removeFirstChild();
}
final Node joinedGroups;
if (spreadParent.hasOneChild() && isSpreadOfArguments(spreadParent.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.
joinedGroups = spreadParent.removeFirstChild().removeFirstChild();
} else {
List groups = extractSpreadGroups(spreadParent);
checkState(!groups.isEmpty());
if (groups.size() == 1) {
// A 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 {
// If the first group is an array literal, we can just use that for concatenation,
// otherwise use an empty array literal.
//
// TODO(nickreid): Stop distringuishing between array literals and variables when this pass
// is moved after type-checking.
Node baseArrayLit = groups.get(0).isArrayLit() ? groups.remove(0) : arrayLitWithJSType();
Node concat = IR.getprop(baseArrayLit, IR.string("concat")).setJSType(concatFnType);
joinedGroups = IR.call(concat, groups.toArray(new Node[0])).setJSType(arrayType);
}
joinedGroups.setJSType(arrayType);
}
boolean isFreeCall = spreadParent.getBooleanProp(Node.FREE_CALL);
final Node callToApply;
if (calleeMayHaveSideEffects && callee.isGetProp() && !isFreeCall) {
JSType receiverType = callee.getFirstChild().getJSType(); // Type of `foo()`.
// foo().method(...[a, b, c])
// must convert to
// var freshVar;
// (freshVar = foo()).method.apply(freshVar, [a, b, c])
Node freshVar =
IR.name(FRESH_SPREAD_VAR + compiler.getUniqueNameIdSupplier().get())
.setJSType(receiverType);
Node freshVarDeclaration = IR.var(freshVar.cloneTree());
Node statementContainingSpread = NodeUtil.getEnclosingStatement(spreadParent);
freshVarDeclaration.useSourceInfoIfMissingFromForTree(statementContainingSpread);
statementContainingSpread
.getParent()
.addChildBefore(freshVarDeclaration, statementContainingSpread);
callee.addChildToFront(
IR.assign(freshVar.cloneTree(), callee.removeFirstChild()).setJSType(receiverType));
callToApply =
IR.call(getpropInferringJSType(callee, "apply"), freshVar.cloneTree(), joinedGroups);
} else {
// foo.method(...[a, b, c]) -> foo.method.apply(foo, [a, b, c])
// 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.isGetElem()) && !isFreeCall
? callee.getFirstChild().cloneTree()
: nullWithJSType();
callToApply = IR.call(getpropInferringJSType(callee, "apply"), context, joinedGroups);
}
callToApply.setJSType(spreadParent.getJSType());
callToApply.useSourceInfoIfMissingFromForTree(spreadParent);
spreadParent.replaceWith(callToApply);
compiler.reportChangeToEnclosingScope(callToApply);
}
private boolean isSpreadOfArguments(Node n) {
return n.isSpread() && n.getOnlyChild().matchesName("arguments");
}
/**
* Processes new calls containing spreads.
*
* Example:
*
*
* new F(...args) =>
* new Function.prototype.bind.apply(F, [].concat($jscomp.arrayFromIterable(args)))
*
*/
private void visitNewWithSpread(Node spreadParent) {
checkArgument(spreadParent.isNew());
// Must remove callee before extracting argument groups.
Node callee = spreadParent.removeFirstChild();
List groups = extractSpreadGroups(spreadParent);
// We need to generate
// `new (Function.prototype.bind.apply(callee, [null].concat(other, args))();`.
// `null` stands in for the 'this' arg to the contructor.
final Node baseArrayLit;
if (groups.get(0).isArrayLit()) {
baseArrayLit = groups.remove(0);
} else {
baseArrayLit = arrayLitWithJSType();
}
baseArrayLit.addChildToFront(nullWithJSType());
Node joinedGroups =
groups.isEmpty()
? baseArrayLit
: IR.call(
IR.getprop(baseArrayLit, IR.string("concat")).setJSType(concatFnType),
groups.toArray(new Node[0]))
.setJSType(arrayType);
if (FeatureSet.ES3.contains(compiler.getOptions().getOutputFeatureSet())) {
// TODO(tbreisacher): Support this in ES3 too by not relying on Function.bind.
Es6ToEs3Util.cannotConvert(
compiler,
spreadParent,
"\"...\" passed to a constructor (consider using --language_out=ES5)");
}
// Function.prototype.bind =>
// function(this:function(new:[spreadParent], ...?), ...?):function(new:[spreadParent])
// Function.prototype.bind.apply =>
// function(function(new:[spreadParent], ...?), !Array>):function(new:[spreadParent])
Node bindApply =
getpropInferringJSType(
getpropInferringJSType(
getpropInferringJSType(
IR.name("Function").setJSType(functionFunctionType), "prototype"),
"bind"),
"apply");
Node result =
IR.newNode(
callInferringJSType(
bindApply, callee, joinedGroups /* function(new:[spreadParent]) */))
.setJSType(spreadParent.getJSType());
result.useSourceInfoIfMissingFromForTree(spreadParent);
spreadParent.replaceWith(result);
compiler.reportChangeToEnclosingScope(result);
}
private Node arrayLitWithJSType() {
return IR.arraylit().setJSType(arrayType);
}
private Node nullWithJSType() {
return IR.nullNode().setJSType(nullType);
}
private Node getpropInferringJSType(Node receiver, String propName) {
Node getprop = IR.getprop(receiver, propName);
JSType receiverType = receiver.getJSType();
if (receiverType == null) {
return getprop;
}
JSType getpropType = receiverType.findPropertyType(propName);
if (getpropType == null && receiverType instanceof FunctionType) {
getpropType = ((FunctionType) receiverType).getPropertyType(propName);
}
return getprop.setJSType(getpropType);
}
private Node callInferringJSType(Node callee, Node... args) {
Node call = IR.call(callee, args);
JSType calleeType = callee.getJSType();
if (calleeType == null || !(calleeType instanceof FunctionType)) {
return call;
}
JSType returnType = ((FunctionType) calleeType).getReturnType();
return call.setJSType(returnType);
}
}