com.google.javascript.jscomp.DestructuringGlobalNameExtractor Maven / Gradle / Ivy
/*
* Copyright 2018 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.checkState;
import com.google.javascript.jscomp.GlobalNamespace.AstChange;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Helper for changing the value of an lvalue in a destructuring pattern. Intended for use by {@link
* CollapseProperties} and {@link AggressiveInlineAliases} only. This class makes some assumptions
* that don't generally hold in order to preserve {@link GlobalNamespace} validity and avoid
* creating temporary variables.
*/
class DestructuringGlobalNameExtractor {
/**
* Given an lvalue in a destructuring pattern, and a detached subtree, rewrites the AST to assign
* the lvalue to the subtree instead of its previous value, while preserving the rest of the
* destructuring pattern
*
* For example: given stringKey: 'y' and newName: 'new.name', where 'y' is contained in the
* declaration `const {x, y, z} = original;`, this method will produce `const {x} = original;
* const y = new.name; const {z} = original;`
*
*
This method only handles a limited subset of destructuring patterns and is intended only for
* CollapseProperties and AggressiveInlineAliases. Preconditions are:
*
*
* - the pattern must be the lhs of an assignment or declaration (e.g. not a nested pattern)
*
- the original rvalue for the pattern must be an effectively constant qualified name. it is
* safe to evaluate the rvalue multiple times.
*
*
* @param stringKey the STRING_KEY node representing the ref to rewrite, e.g. `y`
* @param newName what the lvalue in the STRING_KEY should now be assigned to, e.g. foo.bar
* @param newNodes optionally a set to add new AstChanges to, if you need to keep the
* GlobalNamespace up-to-date. Otherwise null.
* @param ref the Ref corresponding to the `stringKey`
*/
static void reassignDestructringLvalue(
Node stringKey,
Node newName,
@Nullable Set newNodes,
Ref ref,
AbstractCompiler compiler) {
Node pattern = stringKey.getParent();
Node assignmentType = pattern.getParent();
checkState(assignmentType.isAssign() || assignmentType.isDestructuringLhs(), assignmentType);
Node originalRvalue = pattern.getNext(); // e.g. `original`
checkState(originalRvalue.isQualifiedName()); // don't handle rvalues with side effects
// Create a new assignment using the provided qualified name, e.g. `const y = new.name;`
Node lvalueToReassign =
stringKey.getOnlyChild().isDefaultValue()
? stringKey.getOnlyChild().getFirstChild()
: stringKey.getOnlyChild();
if (newNodes != null) {
newNodes.add(new AstChange(ref.module, ref.scope, newName));
}
Node rvalue = makeNewRvalueForDestructuringKey(stringKey, newName, newNodes, ref);
// Add that new assignment to the AST after the original destructuring pattern
if (stringKey.getPrevious() == null && stringKey.getNext() != null) {
// Remove the original item completely if the given string key doesn't have any preceding
// string keys, /and/ it has succeeding string keys.
replaceDestructuringAssignment(pattern, lvalueToReassign.detach(), rvalue);
} else {
addAfter(pattern, lvalueToReassign.detach(), rvalue);
}
// Remove any lvalues in the pattern that are after the given stringKey, and put them in
// a second destructuring pattern. This is necessary in case they depend on side effects from
// assigning `lvalueToReassign`. e.g. create `const {z} = original;`
if (stringKey.getNext() != null) {
Node newPattern = createNewObjectPatternFromSuccessiveKeys(stringKey).srcref(pattern);
// reuse the original rvalue if we don't need it for earlier keys, otherwise make a copy.
final Node newRvalue;
if (stringKey.getPrevious() == null) {
newRvalue = originalRvalue.detach();
} else {
newRvalue = originalRvalue.cloneTree();
if (newNodes != null) {
newNodes.add(new AstChange(ref.module, ref.scope, newRvalue));
}
}
addAfter(lvalueToReassign, newPattern, newRvalue);
}
stringKey.detach();
compiler.reportChangeToEnclosingScope(lvalueToReassign);
}
/** Adds the new assign or name declaration after the original assign or name declaration */
private static void addAfter(Node originalLvalue, Node newLvalue, Node newRvalue) {
Node parent = originalLvalue.getParent();
if (parent.isAssign()) {
// create `(, )`
Node newAssign = IR.assign(newLvalue, newRvalue).srcref(parent);
Node newComma = new Node(Token.COMMA, newAssign);
parent.replaceWith(newComma);
newComma.addChildToFront(parent);
return;
}
// This must have been in a var/let/const.
if (newLvalue.isDestructuringPattern()) {
newLvalue = new Node(Token.DESTRUCTURING_LHS, newLvalue, newRvalue).srcref(parent);
} else {
newLvalue.addChildToBack(newRvalue);
}
Node declaration = parent.isDestructuringLhs() ? originalLvalue.getGrandparent() : parent;
checkState(NodeUtil.isNameDeclaration(declaration), declaration);
if (NodeUtil.isStatementParent(declaration.getParent())) {
// `const {} = originalRvalue; const newLvalue = newRvalue;`
// create an entirely new statement
Node newDeclaration = new Node(declaration.getToken()).srcref(declaration);
newDeclaration.addChildToBack(newLvalue);
declaration.getParent().addChildAfter(newDeclaration, declaration);
} else {
// `const {} = originalRvalue, newLvalue = newRvalue;`
// The Normalize pass tries to ensure name declarations are always in statement blocks, but
// currently has not implemented normalization for `for (let x = 0; ...`
// so we can't add a new statement
declaration.addChildToBack(newLvalue);
}
}
/**
* Replaces the given assignment or declaration with the new lvalue/rvalue
*
* @param pattern a destructuring pattern in an ASSIGN or VAR/LET/CONST
*/
private static void replaceDestructuringAssignment(Node pattern, Node newLvalue, Node newRvalue) {
Node parent = pattern.getParent();
if (parent.isAssign()) {
Node newAssign = IR.assign(newLvalue, newRvalue).srcref(parent);
parent.replaceWith(newAssign);
} else if (newLvalue.isName()) {
checkState(parent.isDestructuringLhs());
parent.replaceWith(newLvalue);
newLvalue.addChildToBack(newRvalue);
} else {
pattern.getNext().detach();
pattern.detach();
parent.addChildToBack(newLvalue);
parent.addChildToBack(newRvalue);
}
}
/**
* Makes a default value expression from the rvalue, or otherwise just returns it
*
* e.g. for `const {x = defaultValue} = y;`, and the new rvalue `rvalue`, returns `void 0 ===
* rvalue ? defaultValue : rvalue`
*/
private static Node makeNewRvalueForDestructuringKey(
Node stringKey, Node rvalue, Set newNodes, Ref ref) {
if (stringKey.getOnlyChild().isDefaultValue()) {
Node defaultValue = stringKey.getFirstChild().getSecondChild().detach();
// Assume `rvalue` has no side effects since it's a qname, and we can create multiple
// references to it. This ignores getters/setters.
Node rvalueForSheq = rvalue.cloneTree();
if (newNodes != null) {
newNodes.add(new AstChange(ref.module, ref.scope, rvalueForSheq));
}
// `void 0 === rvalue ? defaultValue : rvalue`
rvalue =
IR.hook(IR.sheq(NodeUtil.newUndefinedNode(rvalue), rvalueForSheq), defaultValue, rvalue)
.srcrefTree(defaultValue);
}
return rvalue;
}
/** Removes any keys after the given key, and adds them in order to a new object pattern */
private static Node createNewObjectPatternFromSuccessiveKeys(Node stringKey) {
Node newPattern = stringKey.getParent().cloneNode(); // copies the JSType
for (Node next = stringKey.getNext(); next != null; ) {
Node newKey = next;
next = newKey.getNext();
newPattern.addChildToBack(newKey.detach());
}
return newPattern;
}
}