Skip to content

Commit

Permalink
Provide two modes for Es6RewriteDestructuring
Browse files Browse the repository at this point in the history
* rewrite all
* rewrite only patterns containing object rest

and a Builder to specify the desired rewrite mode.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=203027589
  • Loading branch information
MatthewMerrill authored and lauraharker committed Jul 2, 2018
1 parent fc6fb89 commit cc38f71
Show file tree
Hide file tree
Showing 7 changed files with 870 additions and 919 deletions.
280 changes: 251 additions & 29 deletions src/com/google/javascript/jscomp/Es6RewriteDestructuring.java
Expand Up @@ -17,6 +17,7 @@


import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.DiagnosticType.error;
import static com.google.javascript.jscomp.Es6ToEs3Util.arrayFromIterator; import static com.google.javascript.jscomp.Es6ToEs3Util.arrayFromIterator;
import static com.google.javascript.jscomp.Es6ToEs3Util.makeIterator; import static com.google.javascript.jscomp.Es6ToEs3Util.makeIterator;


Expand All @@ -28,46 +29,155 @@
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token; import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream; import com.google.javascript.rhino.TokenStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;


/** /**
* Rewrites ES6 destructuring patterns and default parameters to valid ES3 code. * Rewrites destructuring patterns and default parameters to valid ES3 code or to a different form
* of destructuring.
*/ */
// TODO(mattmm): Rename class to drop Es6 prefix
public final class Es6RewriteDestructuring implements NodeTraversal.Callback, HotSwapCompilerPass { public final class Es6RewriteDestructuring implements NodeTraversal.Callback, HotSwapCompilerPass {

public static final DiagnosticType UNEXPECTED_DESTRUCTURING_REST_PARAMETER =
error(
"JSC_UNEXPECTED_DESTRUCTURING_REST_PARAMETER",
"Es6RewriteDestructuring not expecting object pattern rest parameter");

enum ObjectDestructuringRewriteMode {
/**
* Rewrite all object destructuring patterns. This is the default mode used if no
* ObjectDestructuringRewriteMode is provided to the Builder.
*
* <p>Used to transpile ES2018 -> ES5
*/
REWRITE_ALL_OBJECT_PATTERNS,

/**
* Rewrite only destructuring patterns that contain object pattern rest properties (whether the
* rest is on the top level or nested within a property).
*
* <p>Used to transpile ES2018 -> ES2017
*/
REWRITE_OBJECT_REST,
}

private final AbstractCompiler compiler; private final AbstractCompiler compiler;
private static final FeatureSet transpiledFeatures = private final ObjectDestructuringRewriteMode rewriteMode;
FeatureSet.BARE_MINIMUM.with(
Feature.DEFAULT_PARAMETERS, Feature.DESTRUCTURING, Feature.ARRAY_PATTERN_REST); private final FeatureSet featuresToTriggerRunningPass;
private final FeatureSet featuresToMarkAsRemoved;

private final Deque<PatternNestingLevel> patternNestingStack = new ArrayDeque<>();


static final String DESTRUCTURING_TEMP_VAR = "$jscomp$destructuring$var"; static final String DESTRUCTURING_TEMP_VAR = "$jscomp$destructuring$var";


private int destructuringVarCounter = 0; private int destructuringVarCounter = 0;


public Es6RewriteDestructuring(AbstractCompiler compiler) { private Es6RewriteDestructuring(Builder builder) {
this.compiler = compiler; this.compiler = builder.compiler;
this.rewriteMode = builder.rewriteMode;

switch (this.rewriteMode) {
case REWRITE_ALL_OBJECT_PATTERNS:
this.featuresToTriggerRunningPass =
FeatureSet.BARE_MINIMUM.with(
Feature.DEFAULT_PARAMETERS, Feature.DESTRUCTURING, Feature.ARRAY_PATTERN_REST);

// If OBJECT_PATTERN_REST were to be present in featuresToTriggerRunningPass and not the
// input language featureSet (such as ES6=>ES5) the pass would be skipped.
this.featuresToMarkAsRemoved =
featuresToTriggerRunningPass.with(Feature.OBJECT_PATTERN_REST);
break;
case REWRITE_OBJECT_REST:
// TODO(bradfordcsmith): We shouldn't really need to remove default parameters for this
// case.
this.featuresToTriggerRunningPass =
FeatureSet.BARE_MINIMUM.with(Feature.DEFAULT_PARAMETERS, Feature.OBJECT_PATTERN_REST);
this.featuresToMarkAsRemoved = this.featuresToTriggerRunningPass;
break;
default:
throw new AssertionError(
"Es6RewriteDestructuring cannot handle ObjectDestructuringRewriteMode "
+ this.rewriteMode);
}
}

static class Builder {

private final AbstractCompiler compiler;
private ObjectDestructuringRewriteMode rewriteMode =
ObjectDestructuringRewriteMode.REWRITE_ALL_OBJECT_PATTERNS;

public Builder(AbstractCompiler compiler) {
this.compiler = compiler;
}

public Builder setDestructuringRewriteMode(ObjectDestructuringRewriteMode rewriteMode) {
this.rewriteMode = rewriteMode;
return this;
}

public Es6RewriteDestructuring build() {
return new Es6RewriteDestructuring(this);
}
}

private static final class PatternNestingLevel {

final Node pattern;
boolean hasNestedObjectRest;

public PatternNestingLevel(Node pattern, boolean hasNestedRest) {
this.pattern = pattern;
this.hasNestedObjectRest = hasNestedRest;
}
} }


@Override @Override
public void process(Node externs, Node root) { public void process(Node externs, Node root) {
TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, this); checkState(patternNestingStack.isEmpty());
TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, this); TranspilationPasses.processTranspile(compiler, externs, featuresToTriggerRunningPass, this);
TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures); TranspilationPasses.processTranspile(compiler, root, featuresToTriggerRunningPass, this);
TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, featuresToMarkAsRemoved);
checkState(patternNestingStack.isEmpty());
} }


@Override @Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) { public void hotSwapScript(Node scriptRoot, Node originalRoot) {
TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, transpiledFeatures, this); checkState(patternNestingStack.isEmpty());
TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, transpiledFeatures); TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, featuresToTriggerRunningPass, this);
TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, featuresToMarkAsRemoved);
checkState(patternNestingStack.isEmpty());
} }


@Override @Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) { switch (n.getToken()) {
case FUNCTION: case FUNCTION:
visitFunction(t, n); ensureArrowFunctionsHaveBlockBodies(t, n);
break; break;
case PARAM_LIST: case PARAM_LIST:
visitParamList(n, parent); pullDestructuringOutOfParams(n, parent);
break; break;
case ARRAY_PATTERN:
case OBJECT_PATTERN:
{
boolean hasRest = n.isObjectPattern() && n.hasChildren() && n.getLastChild().isRest();
if (!this.patternNestingStack.isEmpty() && hasRest) {
for (PatternNestingLevel level : patternNestingStack) {
if (level.hasNestedObjectRest) {
break;
}
level.hasNestedObjectRest = true;
}
this.patternNestingStack.peekLast().hasNestedObjectRest = true;
}
this.patternNestingStack.addLast(new PatternNestingLevel(n, hasRest));
break;
}
default: default:
break; break;
} }
Expand All @@ -83,6 +193,9 @@ public void visit(NodeTraversal t, Node n, Node parent) {
case ARRAY_PATTERN: case ARRAY_PATTERN:
case OBJECT_PATTERN: case OBJECT_PATTERN:
visitPattern(t, n, parent); visitPattern(t, n, parent);
if (n == this.patternNestingStack.getLast().pattern) {
this.patternNestingStack.removeLast();
}
break; break;
default: default:
break; break;
Expand All @@ -92,7 +205,8 @@ public void visit(NodeTraversal t, Node n, Node parent) {
/** /**
* If the function is an arrow function, wrap the body in a block if it is not already a block. * If the function is an arrow function, wrap the body in a block if it is not already a block.
*/ */
private void visitFunction(NodeTraversal t, Node function) { // TODO(mattmm): This should be separated from this pass.
private void ensureArrowFunctionsHaveBlockBodies(NodeTraversal t, Node function) {
Node body = function.getLastChild(); Node body = function.getLastChild();
if (!body.isBlock()) { if (!body.isBlock()) {
body.detach(); body.detach();
Expand All @@ -102,10 +216,10 @@ private void visitFunction(NodeTraversal t, Node function) {
} }
} }


/** /** Processes trailing default and rest of function parameters. */
* Processes trailing default and rest parameters. // TODO(bradfordcsmith): Ideally if we're only removing OBJECT_REST, we should only do this when
*/ // the parameter list contains a usage of OBJECT_REST.
private void visitParamList(Node paramList, Node function) { private void pullDestructuringOutOfParams(Node paramList, Node function) {
Node insertSpot = null; Node insertSpot = null;
Node body = function.getLastChild(); Node body = function.getLastChild();
int i = 0; int i = 0;
Expand Down Expand Up @@ -227,7 +341,8 @@ private void visitPattern(NodeTraversal t, Node pattern, Node parent) {
} else if (parent.isRest() } else if (parent.isRest()
|| parent.isStringKey() || parent.isStringKey()
|| parent.isArrayPattern() || parent.isArrayPattern()
|| parent.isDefaultValue()) { || parent.isDefaultValue()
|| parent.isComputedProp()) {
// Nested pattern; do nothing. We will visit it after rewriting the parent. // Nested pattern; do nothing. We will visit it after rewriting the parent.
} else if (NodeUtil.isEnhancedFor(parent) || NodeUtil.isEnhancedFor(parent.getParent())) { } else if (NodeUtil.isEnhancedFor(parent) || NodeUtil.isEnhancedFor(parent.getParent())) {
visitDestructuringPatternInEnhancedFor(pattern); visitDestructuringPatternInEnhancedFor(pattern);
Expand Down Expand Up @@ -260,6 +375,24 @@ private void replacePattern(
private void replaceObjectPattern( private void replaceObjectPattern(
NodeTraversal t, Node objectPattern, Node rhs, Node parent, Node nodeToDetach) { NodeTraversal t, Node objectPattern, Node rhs, Node parent, Node nodeToDetach) {
String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
String restTempVarName = null;
// If the last child is a rest node we will want a list of the stated properties so we can
// exclude them from being written to the rest variable.
ArrayList<Node> propsToDeleteForRest = null;
if (objectPattern.hasChildren() && objectPattern.getLastChild().isRest()) {
propsToDeleteForRest = new ArrayList<>();
restTempVarName = DESTRUCTURING_TEMP_VAR + destructuringVarCounter++;
} else if (rewriteMode == ObjectDestructuringRewriteMode.REWRITE_OBJECT_REST) {
// We are configured to only break object pattern rest, but this destructure has none.
if (!this.patternNestingStack.peekLast().hasNestedObjectRest) {
// Replacement is performed after the post-order visit has reached the root pattern node, so
// peeking last represents if there is a rest property anywhere in the entire pattern. All
// nesting levels of lower levels have already been popped.
destructuringVarCounter--;
return;
}
}

Node tempDecl = IR.var(IR.name(tempVarName), rhs.detach()) Node tempDecl = IR.var(IR.name(tempVarName), rhs.detach())
.useSourceInfoIfMissingFromForTree(objectPattern); .useSourceInfoIfMissingFromForTree(objectPattern);
// TODO(tbreisacher): Remove the "if" and add this JSDoc unconditionally. // TODO(tbreisacher): Remove the "if" and add this JSDoc unconditionally.
Expand Down Expand Up @@ -296,28 +429,64 @@ private void replaceObjectPattern(
Node defaultValue = value.removeFirstChild(); Node defaultValue = value.removeFirstChild();
newRHS = defaultValueHook(getprop, defaultValue); newRHS = defaultValueHook(getprop, defaultValue);
} }
if (propsToDeleteForRest != null) {
Node propName = IR.string(child.getString());
if (child.isQuotedString()) {
propName.setQuotedString();
}
propsToDeleteForRest.add(propName);
}
} else if (child.isComputedProp()) { } else if (child.isComputedProp()) {
if (child.getLastChild().isDefaultValue()) { boolean hasDefault = child.getLastChild().isDefaultValue();
newLHS = child.getLastChild().removeFirstChild(); Node defaultNode = null;
Node getelem = IR.getelem(IR.name(tempVarName), child.removeFirstChild()); Node defaultValue = null;

Node propExpr = child.removeFirstChild();
if (hasDefault) {
defaultNode = child.getLastChild();
newLHS = defaultNode.removeFirstChild();
defaultValue = defaultNode.removeFirstChild();
} else {
newLHS = child.removeFirstChild();
}
if (propsToDeleteForRest != null) {
// A "...rest" variable is present and result of computation must be cached
String exprEvalTempVarName = DESTRUCTURING_TEMP_VAR + destructuringVarCounter++;
Node exprEvalDecl = IR.var(IR.name(exprEvalTempVarName), propExpr);
exprEvalDecl.useSourceInfoIfMissingFromForTree(child);
nodeToDetach.getParent().addChildBefore(exprEvalDecl, nodeToDetach);
propExpr = IR.name(exprEvalTempVarName);
propsToDeleteForRest.add(IR.name(exprEvalTempVarName));
}
if (hasDefault) {
Node getelem = IR.getelem(IR.name(tempVarName), propExpr);
String intermediateTempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); String intermediateTempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
Node intermediateDecl = IR.var(IR.name(intermediateTempVarName), getelem); Node intermediateDecl = IR.var(IR.name(intermediateTempVarName), getelem);
intermediateDecl.useSourceInfoIfMissingFromForTree(child); intermediateDecl.useSourceInfoIfMissingFromForTree(child);
nodeToDetach.getParent().addChildBefore(intermediateDecl, nodeToDetach); nodeToDetach.getParent().addChildBefore(intermediateDecl, nodeToDetach);

newRHS = defaultValueHook(IR.name(intermediateTempVarName), defaultValue);
newRHS =
defaultValueHook(
IR.name(intermediateTempVarName), child.getLastChild().removeFirstChild());
} else { } else {
newRHS = IR.getelem(IR.name(tempVarName), child.removeFirstChild()); newRHS = IR.getelem(IR.name(tempVarName), propExpr);
newLHS = child.removeFirstChild();
} }
} else if (child.isDefaultValue()) { } else if (child.isDefaultValue()) {
newLHS = child.removeFirstChild(); newLHS = child.removeFirstChild();
Node defaultValue = child.removeFirstChild(); Node defaultValue = child.removeFirstChild();
Node getprop = IR.getprop(IR.name(tempVarName), IR.string(newLHS.getString())); Node getprop = IR.getprop(IR.name(tempVarName), IR.string(newLHS.getString()));
newRHS = defaultValueHook(getprop, defaultValue); newRHS = defaultValueHook(getprop, defaultValue);
if (propsToDeleteForRest != null) {
propsToDeleteForRest.add(IR.stringKey(newLHS.getString()));
}
} else if (child.isRest()) {
if (next != null) {
throw new IllegalStateException("object rest may not be followed by any properties");
}
Node assignCall = IR.call(NodeUtil.newQName(compiler, "Object.assign"));
assignCall.addChildToBack(IR.objectlit());
assignCall.addChildToBack(IR.name(tempVarName));
Node restTempDecl = IR.var(IR.name(restTempVarName), assignCall);
restTempDecl.useSourceInfoIfMissingFromForTree(objectPattern);
nodeToDetach.getParent().addChildAfter(restTempDecl, tempDecl);
newLHS = IR.name(child.getOnlyChild().getString());
newRHS = objectPatternRestRHS(objectPattern, child, restTempVarName, propsToDeleteForRest);
} else { } else {
throw new IllegalStateException("unexpected child"); throw new IllegalStateException("unexpected child");
} }
Expand All @@ -343,6 +512,52 @@ private void replaceObjectPattern(
t.reportCodeChange(); t.reportCodeChange();
} }


/**
* Convert "rest" of object destructuring lhs by making a clone and deleting any properties that
* were stated in the original object pattern.
*
* <p>Nodes in statedProperties that are a stringKey will be used in a getprop when deleting. All
* other types will be used in a getelem such as what is done for computed properties.
*
* <pre>
* {a, [foo()]:b, ...x} = rhs;
* becomes
* var temp = rhs;
* var temp1 = Object.assign({}, temp);
* var temp2 = foo()
* a = temp.a
* b = temp[foo()]
* x = (delete temp1.a, delete temp1[temp2], temp1);
* </pre>
*
* @param rest node representing the "...rest" of objectPattern
* @param restTempVarName name of var containing clone of result of rhs evaluation
* @param statedProperties list of properties to delete from the clone
*/
private Node objectPatternRestRHS(
Node objectPattern, Node rest, String restTempVarName, ArrayList<Node> statedProperties) {
checkArgument(objectPattern.getLastChild() == rest);
Node result = IR.name(restTempVarName);
if (!statedProperties.isEmpty()) {
Iterator<Node> propItr = statedProperties.iterator();
Node comma = deletionNodeForRestProperty(restTempVarName, propItr.next());
while (propItr.hasNext()) {
comma = IR.comma(comma, deletionNodeForRestProperty(restTempVarName, propItr.next()));
}
result = IR.comma(comma, result);
}
result.useSourceInfoIfMissingFromForTree(rest);
return result;
}

private Node deletionNodeForRestProperty(String restTempVarName, Node property) {
boolean useSquareBrackets = !property.isString() || property.isQuotedString();
return new Node(
Token.DELPROP,
new Node(
useSquareBrackets ? Token.GETELEM : Token.GETPROP, IR.name(restTempVarName), property));
}

/** /**
* Convert <pre>var [x, y] = rhs<pre> to: * Convert <pre>var [x, y] = rhs<pre> to:
* <pre> * <pre>
Expand All @@ -353,6 +568,13 @@ private void replaceObjectPattern(
*/ */
private void replaceArrayPattern( private void replaceArrayPattern(
NodeTraversal t, Node arrayPattern, Node rhs, Node parent, Node nodeToDetach) { NodeTraversal t, Node arrayPattern, Node rhs, Node parent, Node nodeToDetach) {

if (rewriteMode == ObjectDestructuringRewriteMode.REWRITE_OBJECT_REST) {
if (patternNestingStack.isEmpty() || !patternNestingStack.peekLast().hasNestedObjectRest) {
return;
}
}

String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++); String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
Node tempDecl = IR.var( Node tempDecl = IR.var(
IR.name(tempVarName), IR.name(tempVarName),
Expand Down

0 comments on commit cc38f71

Please sign in to comment.