All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.Es6RewriteBlockScopedDeclaration Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

The newest version!
/*
 * 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.checkState;
import static com.google.javascript.jscomp.AstFactory.type;

import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.colors.StandardColors;
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.Node;
import com.google.javascript.rhino.Token;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

/**
 * Rewrite "let"s and "const"s as "var"s. Rename block-scoped declarations and their references when
 * necessary.
 *
 * 

Note that this must run after Es6RewriteDestructuring, since it does not process destructuring * let/const declarations at all. */ public final class Es6RewriteBlockScopedDeclaration extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; private final AstFactory astFactory; private final Set letConsts = new LinkedHashSet<>(); private static final FeatureSet transpiledFeatures = FeatureSet.BARE_MINIMUM.with(Feature.LET_DECLARATIONS, Feature.CONST_DECLARATIONS); private final UniqueIdSupplier uniqueIdSupplier; private static final String LOOP_PARAM_NAME_PREFIX = "$jscomp$loop_param$"; public Es6RewriteBlockScopedDeclaration(AbstractCompiler compiler) { this.compiler = compiler; this.uniqueIdSupplier = compiler.getUniqueIdSupplier(); this.astFactory = compiler.createAstFactory(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.hasChildren() || !NodeUtil.isBlockScopedDeclaration(n.getFirstChild())) { return; } // NOTE: This pass depends on for-of being transpiled away before it runs. checkState(parent == null || !parent.isForOf(), parent); if (n.isLet() || n.isConst()) { letConsts.add(n); } if (NodeUtil.isNameDeclaration(n)) { for (Node nameNode = n.getFirstChild(); nameNode != null; nameNode = nameNode.getNext()) { visitBlockScopedNameDeclaration(n, nameNode); } } else { // NOTE: This pass depends on class declarations having been transpiled away checkState(n.isFunction() || n.isCatch(), "Unexpected declaration node: %s", n); visitBlockScopedNameDeclaration(n, n.getFirstChild()); } } @Override public void process(Node externs, Node root) { // Make all declared names unique, so we can safely just switch 'let' or 'const' to 'var' in all // non-loop cases. MakeDeclaredNamesUnique renamer = MakeDeclaredNamesUnique.builder().build(); NodeTraversal.traverseRoots(compiler, renamer, externs, root); // - Gather a list of let & const variables // - Also add `= void 0` to any that are not initialized. NodeTraversal.traverse(compiler, root, this); LoopClosureTransformer transformer = new LoopClosureTransformer(); NodeTraversal.traverse(compiler, root, transformer); transformer.transformLoopClosure(); rewriteDeclsToVars(); TranspilationPasses.maybeMarkFeaturesAsTranspiledAway(compiler, root, transpiledFeatures); } /** * Renames block-scoped declarations that shadow a variable in an outer scope * *

Also normalizes declarations with no initializer in a loop to be initialized to undefined. */ private void visitBlockScopedNameDeclaration(Node decl, Node nameNode) { Node parent = decl.getParent(); // Normalize "let x;" to "let x = undefined;" if in a loop, since we later convert x // to be $jscomp$loop$0.x and want to reset the property to undefined every loop iteration. if ((decl.isLet() || decl.isConst()) && !nameNode.hasChildren() && (parent == null || !parent.isForIn()) && inLoop(decl)) { Node undefined = astFactory.createUndefinedValue().srcrefTree(nameNode); nameNode.addChildToFront(undefined); compiler.reportChangeToEnclosingScope(undefined); } } /** * Whether n is inside a loop. If n is inside a function which is inside a loop, we do not * consider it to be inside a loop. */ private boolean inLoop(Node n) { Node enclosingNode = NodeUtil.getEnclosingNode(n, isLoopOrFunction); return enclosingNode != null && !enclosingNode.isFunction(); } private static final Predicate isLoopOrFunction = (n) -> n.isFunction() || NodeUtil.isLoopStructure(n); private static void extractInlineJSDoc(Node srcDeclaration, Node srcName, Node destDeclaration) { JSDocInfo existingInfo = srcDeclaration.getJSDocInfo(); if (existingInfo == null) { // Extract inline JSDoc from "src" and add it to the "dest" node. existingInfo = srcName.getJSDocInfo(); srcName.setJSDocInfo(null); } JSDocInfo.Builder builder = JSDocInfo.Builder.maybeCopyFrom(existingInfo); destDeclaration.setJSDocInfo(builder.build()); } private static void maybeAddConstJSDoc(Node srcDeclaration, Node srcName, Node destDeclaration) { if (srcDeclaration.isConst()) { extractInlineJSDoc(srcDeclaration, srcName, destDeclaration); JSDocInfo.Builder builder = JSDocInfo.Builder.maybeCopyFrom(destDeclaration.getJSDocInfo()); builder.recordConstancy(); destDeclaration.setJSDocInfo(builder.build()); } } /** * Given a declarationList of let/const declarations, convert all declared names to their own, * separate (normalized) var declarations. * *

"const i = 0, j = 0;" becomes "/\*\* @const *\/ var i = 0; /\** @const *\/ var j = 0;" * *

If the declarationList of let/const declarations is in a FOR intializer, moves those into * separate declarations outside the FOR loop (for maintaining normalization). * *

TODO: b/197349249 - We won't have any declaration lists here when this pass runs post * normalize. */ private void handleDeclarationList(Node declarationList, Node parent) { if (declarationList.isVar() && declarationList.hasOneChild()) { // This declaration list is already handled and we can safely return. This happens because // this method is also called by {@code replaceDeclarationWithProperty} where it gets // repeatedly called for each name in the declarationList. After the first call // to this (for the first name), this declarationList would no longer be {@code Token.CONST} // (i.e. it would've become a separate var with an {@code /** const */} annotation). return; } if (parent.isVanillaFor()) { /* * This is needed to handle the case where we get: * *

    *
  1. `for (let x = ...)` or *
  2. `for (const x = ...`. < *
* *

Normalize only moves "var" outside the for initializer, it allows let/const within the * initializer. So both #1 and #2 can exist regardless if this pass runs before and after * normalize. */ handleDeclarationListInVanillaForInitializer(declarationList, parent); return; } if (parent.isForIn()) { /* * This is needed to handle the case where we get: * *

    *
  1. `for (let x in ...)` or *
  2. `for (const x in ...`. < *
* * *

Normalize only moves "var" outside the for initializer, it allows let/const within the * initializer. So both #1 and #2 can exist regardless if this pass runs before and after * normalize. */ handleDeclarationListInForInInitializer(declarationList, parent); return; } // convert all names to their own, separate (normalized) declarations while (declarationList.hasChildren()) { Node name = declarationList.getLastChild(); Node newDeclaration = IR.var(name.detach()).srcref(declarationList); /* * This method gets called multiple times by {@code replaceDeclarationWithProperty}. In the * first call, it splits the declarationList into individual /\** @const *\/ var declarations. * i.e. `const a,b` --> `/\** @const *\/ var a; /\** @const *\/ var b;` * *

Then it gets invoked for each individual var declaration. once for each name in the * original declarationList). However, after the first call to this (for the first name), this * declarationList would no longer be {@code Token.CONST} (i.e. it would've become a separate * var with an {@code /*\* const *\/} annotation). * *

Previously, this method rewrote those individual var declarations again into new var * declarations. Without copying over the JSDoc, those newly created var declarations did not * contain `@const`, and produce this: * *

`/\** @const *\/ var a; /\** @const *\/ var b;` -->`var a; var b;` * *

Now, even though the "redundant rewriting" is handled by early-returning from this * method whenever this method gets called with a non-declaration var list, it's still * important that the individual var declarations created by the first rewriting contain the * annotations from original declaration list. Hence we propagate JSDoc from declarationList * into the individual var declarations. */ extractInlineJSDoc(declarationList, name, newDeclaration); maybeAddConstJSDoc(declarationList, name, newDeclaration); newDeclaration.insertAfter(declarationList); compiler.reportChangeToEnclosingScope(parent); } // declarationList has no children left. Remove. declarationList.detach(); compiler.reportChangeToEnclosingScope(parent); } /** * TODO: b/197349249 - We won't have any declaration lists in the FOR_IN initializer here when * this pass runs post normalize. After that, all lets/consts can be directly converted to vars. */ private void handleDeclarationListInForInInitializer(Node declarationList, Node parent) { checkState(parent.isForIn()); Node first = declarationList; Node lhs = first.getFirstChild(); Node loopNode = parent; if (lhs.isDestructuringLhs()) { // This pass relies on destructuring syntax being already removed. Hence we must not enter // this case. // TODO: b/279640656 Enable this code path once this pass runs unconditionally in stage3. checkState( false, "Destructuring syntax is unsupported in ES6RewriteBlockScopedDeclarations pass"); // Transform: // `for (let [a, b = 3] in c) {}` or `for (const [a, b = 3] in c) {}` // to: //

      //    `var a; var b; for ([a, b = 3] in c) {}`
      // 
// respectively NodeUtil.visitLhsNodesInNode( lhs, (name) -> { // Add a declaration outside the for loop for the given name. checkState( name.isName(), "lhs in destructuring declaration should be a simple name. (%s)", name); Node newName = IR.name(name.getString()).srcref(name); Node newVar = IR.var(newName).srcref(name); extractInlineJSDoc(declarationList, name, newVar); // if the initializer name was a const, the newName must no longer be a const. addNodeBeforeLoop(newVar, loopNode); }); // Transform `for (var [a, b]... )` to `for ([a, b]...` Node destructuringPattern = lhs.removeFirstChild(); first.replaceWith(destructuringPattern); } else { // Transform: // for (let a in b) {} // to: // var a; for (a in b) {}; // and: // for (const a in b) {} // to: // var a; for (a in b) {}; Node newStatement = first.cloneTree(); Node name = newStatement.getFirstChild().cloneNode(); // cloning also copies over any properties if (name.getBooleanProp(Node.IS_CONSTANT_NAME)) { // if the initializer name was a const, it must no longer be a marked const. However, // marking it as non-const requires us to also change all references to the initializer name // in the for-in's body. That is expensive to do. Hence, we leave the initializer name and // its references within the loop to be const (trade off optimizability for compile time). } newStatement.setToken(Token.VAR); extractInlineJSDoc(first, first.getFirstChild(), newStatement); first.replaceWith(name); addNodeBeforeLoop(newStatement, loopNode); } } /** * TODO: b/197349249 - We won't have any declaration lists in the FOR initializer here when this * pass runs post normalize. After that, all lets/consts can be directly converted to vars. */ private void handleDeclarationListInVanillaForInitializer(Node declarationList, Node parent) { checkState(parent.isVanillaFor()); // if the declarationList is in a FOR initializer, move it outside Node insertSpot = getInsertSpotBeforeLoop(parent); // convert all names to their own, separate (normalized) declarations while (declarationList.hasChildren()) { Node name = declarationList.getLastChild(); Node newDeclaration = IR.var(name.detach()).srcref(declarationList); extractInlineJSDoc(declarationList, name, newDeclaration); if (name.getBooleanProp(Node.IS_CONSTANT_NAME)) { // if the initializer name was a const, it must no longer be a marked const. However, // marking it as non-const requires us to also change all references to the initializer name // in the for's body. That is expensive to do. Hence, we leave the initializer name and // its references within the loop to be const (trade off optimizability for compile time). } // generate normalized var initializer (i.e. outside FOR) newDeclaration.insertBefore(insertSpot); insertSpot = newDeclaration; compiler.reportChangeToEnclosingScope(parent); } // make FOR initializer empty `for (; cond; incr)` Node empty = astFactory.createEmpty().srcref(declarationList); declarationList.replaceWith(empty); compiler.reportChangeToEnclosingScope(empty); } private void addNodeBeforeLoop(Node newNode, Node loopNode) { Node insertSpot = getInsertSpotBeforeLoop(loopNode); newNode.insertBefore(insertSpot); compiler.reportChangeToEnclosingScope(newNode); } private Node getInsertSpotBeforeLoop(Node loopNode) { Node insertSpot = loopNode; while (insertSpot.getParent().isLabel()) { insertSpot = insertSpot.getParent(); } return insertSpot; } private void rewriteDeclsToVars() { if (!letConsts.isEmpty()) { for (Node n : letConsts) { // for both lets and consts we want to split the declaration lists when converting them to // vars (to maintain normalization) handleDeclarationList(n, n.getParent()); } } } /** Transforms let/const declarations captured by loop closures. */ private class LoopClosureTransformer extends AbstractPreOrderCallback { private static final String LOOP_OBJECT_NAME = "$jscomp$loop"; private final Map loopObjectMap = new LinkedHashMap<>(); private final SetMultimap nodesRequiringLoopObjectsClosureMap = LinkedHashMultimap.create(); private final SetMultimap nodesHandledForLoopObjectClosure = HashMultimap.create(); private final SetMultimap referenceMap = LinkedHashMultimap.create(); @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { if (!NodeUtil.isReferenceName(n)) { return true; } String name = n.getString(); Scope referencedIn = t.getScope(); Var var = referencedIn.getVar(name); if (var == null) { return true; } if (!var.isLet() && !var.isConst()) { return true; } // Traverse nodes up from let/const declaration: // If we hit a function or the root before a loop - Not a loop closure. // if we hit a loop first - maybe loop closure. Scope declaredIn = var.getScope(); Node loopNode = null; for (Scope s = declaredIn; ; s = s.getParent()) { Node scopeRoot = s.getRootNode(); if (NodeUtil.isLoopStructure(scopeRoot)) { loopNode = scopeRoot; break; } else if (scopeRoot.hasParent() && NodeUtil.isLoopStructure(scopeRoot.getParent())) { loopNode = scopeRoot.getParent(); break; } else if (s.isFunctionBlockScope() || s.isGlobal()) { return true; } } referenceMap.put(var, n); // Traverse scopes from reference scope to declaration scope. // If we hit a function - loop closure detected. Scope outerMostFunctionScope = null; for (Scope s = referencedIn; s != declaredIn && s.getRootNode() != loopNode; s = s.getParent()) { if (s.isFunctionScope()) { outerMostFunctionScope = s; } } if (outerMostFunctionScope != null) { Node enclosingFunction = outerMostFunctionScope.getRootNode(); // There are two categories of functions we might find here: // 1. a getter or setter in an object literal. We will wrap the entire object literal in // a closure to capture the value of the let/const. // 2. a function declaration or expression. We will wrap the function in a closure. // (At this point, class methods/getters/setters and object literal member functions are // transpiled away.) final Node nodeToWrapInClosure; if (enclosingFunction.getParent().isGetterDef() || enclosingFunction.getParent().isSetterDef()) { nodeToWrapInClosure = enclosingFunction.getGrandparent(); checkState(nodeToWrapInClosure.isObjectLit()); } else { nodeToWrapInClosure = enclosingFunction; } if (nodesHandledForLoopObjectClosure.containsEntry(nodeToWrapInClosure, name)) { return true; } nodesHandledForLoopObjectClosure.put(nodeToWrapInClosure, name); LoopObject object = loopObjectMap.computeIfAbsent( loopNode, (Node k) -> new LoopObject(createUniqueObjectName(t.getInput()))); object.vars.add(var); nodesRequiringLoopObjectsClosureMap.put(nodeToWrapInClosure, object); } return true; } private String createUniqueObjectName(CompilerInput input) { return LOOP_OBJECT_NAME + "$" + uniqueIdSupplier.getUniqueId(input); } private String getLoopObjPropName(Var var) { // NOTE: var.getName() would be wrong here, because it will still contain the original // and possibly non-unique name for the variable. However, the name node itself will already // have the new and guaranteed-globally-unique name. return var.getNameNode().getString(); } private void transformLoopClosure() { if (loopObjectMap.isEmpty()) { return; } Set wrapperFunctions = createWrapperFunctions(); for (Node loopNode : loopObjectMap.keySet()) { // Introduce objects to reflect the captured scope variables. // Fields are initially left as undefined to avoid cases like: // var $jscomp$loop$0 = {$jscomp$loop$prop$i: 0, $jscomp$loop$prop$j: $jscomp$loop$0.i} // They are initialized lazily by changing declarations into assignments // later. LoopObject loopObject = loopObjectMap.get(loopNode); Node objectLitNextIteration = astFactory.createObjectLit(); renameVarsToProperties(loopObject, objectLitNextIteration, loopNode); Node updateLoopObject = astFactory.createAssign(createLoopObjectNameNode(loopObject), objectLitNextIteration); Node objectLit = // This is the only time we generate a loop object variable for a loopNode. IR.var(createLoopObjectNameNode(loopObject), astFactory.createObjectLit()) .srcrefTree(loopNode); addNodeBeforeLoop(objectLit, loopNode); if (loopNode.isVanillaFor()) { // For changeVanillaForLoopHeader(loopNode, updateLoopObject); } else { final Node loopBody = NodeUtil.getLoopCodeBlock(loopNode); loopBody.addChildToFront(IR.exprResult(updateLoopObject).srcrefTreeIfMissing(loopNode)); } compiler.reportChangeToEnclosingScope(loopNode); changeLoopLocalVariablesToProperties(loopNode, loopObject); } // At this point, all local variables in the loop have been changed to property accesses on // the "loopObject" name. For the wrapper functions that we introduced in the loop, we must // change the name references in their body to refer to their parameter name instead of the // "loopObject" name. // TODO(bradfordcsmith): This is inefficient. We should really choose the names first, then // make the changes, instead of changing the same variable references twice. updateNamesInWrapperFunctions(wrapperFunctions); } /** * Before: * *
{@code
     * var arr = [];
     * var LOOP$0 = {};
     * var i = 0;
     * for (; i < 10; LOOP$0 = {y: LOOP$0.y}, i++) {
     *   LOOP$0.y = i;
     *   arr.push((function(LOOP$0$PARAM$1) {
     *       return function() { return LOOP$0.y; }; <---- must use param name
     *   })(LOOP$0));
     * }
     *
     * }
* * After: * *
{@code
     * var arr = [];
     * var LOOP$0 = {};
     * var i = 0;
     * for (; i < 10; LOOP$0 = {y: LOOP$0.y}, i++) {
     *   LOOP$0.y = i;
     *   arr.push((function(LOOP$0$PARAM$1) {
     *       return function() { return LOOP$0$PARAM$1.y; }; <--- changed
     *   })(LOOP$0));
     * }
     *
     * }
*/ private void updateNamesInWrapperFunctions(Set wrapperFunctions) { for (Node func : wrapperFunctions) { // get the param names here (and the loopObject names from it) Node paramList = func.getSecondChild(); for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) { String loopObjectName = param.getString().substring(0, param.getString().indexOf(LOOP_PARAM_NAME_PREFIX)); updateNames(/* block */ func.getLastChild(), param, loopObjectName); } } } /** Create wrapper functions and call them. */ private Set createWrapperFunctions() { Set wrapperFunctions = new LinkedHashSet<>(); for (Node functionOrObjectLit : nodesRequiringLoopObjectsClosureMap.keySet()) { Node returnNode = IR.returnNode(); Set objects = nodesRequiringLoopObjectsClosureMap.get(functionOrObjectLit); Node[] parameterNames = new Node[objects.size()]; Node[] objectNamesForCall = new Node[objects.size()]; int loopObjectIndex = 0; for (LoopObject object : objects) { // This name for parameter should be unique to preserve normalization parameterNames[loopObjectIndex] = createUniqueParameterNameToUseWithinLoop(functionOrObjectLit, object); // this name must be the same as the loop object name objectNamesForCall[loopObjectIndex] = createLoopObjectNameNode(object); loopObjectIndex++; } Node iife = astFactory.createFunction( "", IR.paramList(parameterNames), IR.block(returnNode), type(StandardColors.TOP_OBJECT)); compiler.reportChangeToChangeScope(iife); Node call = astFactory.createCall(iife, type(functionOrObjectLit), objectNamesForCall); call.putBooleanProp(Node.FREE_CALL, true); Node replacement; // Es6RewriteBlockScopedFunctionDeclaration must run before this pass. checkState( !NodeUtil.isFunctionDeclaration(functionOrObjectLit), "block-scoped function declarations should not exist now: %s", functionOrObjectLit); replacement = call.srcrefTreeIfMissing(functionOrObjectLit); functionOrObjectLit.replaceWith(replacement); wrapperFunctions.add(iife); returnNode.addChildToFront(functionOrObjectLit); compiler.reportChangeToEnclosingScope(replacement); } return wrapperFunctions; } /* * Renames all references within the functionOrObjectLit body from the old loopObjectName * (e.g. "$jscomp$loop$1") to the given loopObjectParamName * (e.g. $jscomp$loop$1$jscomp$loop_param$m123..456). */ private void updateNames(Node node, Node paramName, String loopObjectName) { if (node.isName() && !node.getString().isEmpty() /* not an anonymous function name */) { if (node.getString().contains(loopObjectName)) { node.setString(paramName.getString()); return; } } for (Node child = node.getFirstChild(); child != null; child = child.getNext()) { updateNames(child, paramName, loopObjectName); } } /** * The initializer is pulled out and placed prior to the loop. The increment is updated with the * new loop object and property assignments */ private void changeVanillaForLoopHeader(Node loopNode, Node updateLoopObject) { Node initializer = loopNode.getFirstChild(); initializer.replaceWith(IR.empty()); if (!initializer.isEmpty()) { if (!NodeUtil.isNameDeclaration(initializer)) { initializer = IR.exprResult(initializer).srcref(initializer); } addNodeBeforeLoop(initializer, loopNode); } Node increment = loopNode.getChildAtIndex(2); if (increment.isEmpty()) { increment.replaceWith(updateLoopObject.srcrefTreeIfMissing(loopNode)); } else { Node placeHolder = IR.empty(); increment.replaceWith(placeHolder); placeHolder.replaceWith( astFactory.createComma(updateLoopObject, increment).srcrefTreeIfMissing(loopNode)); } } /** * For captured variables, change declarations to assignments on the corresponding field of the * introduced object. Rename all references accordingly. */ private void changeLoopLocalVariablesToProperties(Node loopNode, LoopObject loopObject) { for (Var var : loopObject.vars) { String newPropertyName = getLoopObjPropName(var); for (Node reference : referenceMap.get(var)) { // for-of loops are transpiled away before this pass runs checkState(!loopNode.isForOf(), loopNode); // For-of and for-in declarations are not altered, since they are // used as temporary variables for assignment. if (loopNode.isForIn() && loopNode.getFirstChild() == reference.getParent()) { assignLoopVarToLoopObjectProperty( loopNode, loopObject, var, newPropertyName, reference); } else { if (NodeUtil.isNameDeclaration(reference.getParent())) { replaceDeclarationWithProperty(loopObject, newPropertyName, reference); } else { replaceReferenceWithProperty(loopObject, newPropertyName, reference); } } } } } private void replaceDeclarationWithProperty( LoopObject loopObject, String newPropertyName, Node reference) { Node declaration = reference.getParent(); Node grandParent = declaration.getParent(); // Record that the let / const declaration statement will get turned into one or more // var statements by handleDeclarationList(), so we won't try to change it again later. letConsts.remove(declaration); // If the declaration contains multiple declared variables, split it apart. // NOTE: This call could be made for each declarationList once, rather than each name in that // list handleDeclarationList(declaration, grandParent); // The variable we're working with may have been moved to a new var statement. declaration = reference.getParent(); if (reference.hasChildren()) { // Change declaration to assignment Node newReference = createLoopVarReferenceReplacement(loopObject, reference, newPropertyName); Node assign = astFactory.createAssign(newReference, reference.removeFirstChild()); extractInlineJSDoc(declaration, reference, declaration); maybeAddConstJSDoc(declaration, reference, declaration); assign.setJSDocInfo(declaration.getJSDocInfo()); Node replacement = IR.exprResult(assign).srcrefTreeIfMissing(declaration); declaration.replaceWith(replacement); } else { // No value is assigned, so just drop the let/const statement entirely declaration.detach(); } compiler.reportChangeToEnclosingScope(grandParent); } private void replaceReferenceWithProperty( LoopObject loopObject, String newPropertyName, Node reference) { Node referenceParent = reference.getParent(); reference.replaceWith( createLoopVarReferenceReplacement(loopObject, reference, newPropertyName)); compiler.reportChangeToEnclosingScope(referenceParent); } /** * Transforms `for (const p in obj) { ... }` * *

into `for (const p in obj) { $jscomp$loop$0.$jscomp$loop$prop$0$p = p; ... }` */ private void assignLoopVarToLoopObjectProperty( Node loopNode, LoopObject loopObject, Var var, String newPropertyName, Node reference) { // reference is the node loopVar in a for-in that looks like this: // `for (const loopVar in list) {` checkState(reference == var.getNameNode(), reference); Node referenceParent = reference.getParent(); checkState(NodeUtil.isNameDeclaration(referenceParent), referenceParent); checkState(reference.isName(), reference); // Start transpiled form of // `for (const p in obj) { ... }` // with this statement to copy the loop variable into the corresponding loop object // property. // `$jscomp$loop$0.$jscomp$loop$prop$0$p = p;` Node loopVarReference = reference.cloneNode(); // `$jscomp$loop$0.$jscomp$loop$prop$0$p = p;` final Node forInPropAssignmentStatemnt = IR.exprResult( astFactory.createAssign( createLoopVarReferenceReplacement(loopObject, reference, newPropertyName), loopVarReference)) .srcrefTreeIfMissing(reference); // The first statement in the body should be creating a new loop object value // $jscomp$loop$0 = { // $jscomp$loop$prop$0$p: $jscomp$loop$0.$jscomp$loop$prop$0$p, // $jscomp$loop$prop$0$otherVar: $jscomp$loop$0.$jscomp$loop$prop$0$p, // // other property update assignments // } // We need to update the loop variable's value to it immediately after that final Node loopUpdateStatement = loopNode .getLastChild() // loop body .getFirstChild(); // first statement forInPropAssignmentStatemnt.insertAfter(loopUpdateStatement); } /** Rename all variables in the loop object to properties */ private void renameVarsToProperties( LoopObject loopObject, Node objectLitNextIteration, Node scopeRoot) { for (Var var : loopObject.vars) { String newPropertyName = getLoopObjPropName(var); Node newPropertyValue = var.getScopeRoot() == scopeRoot ? createLoopVarReferenceReplacement(loopObject, var.getNameNode(), newPropertyName) : astFactory.createUndefinedValue(); objectLitNextIteration.addChildToBack( astFactory.createStringKey(newPropertyName, newPropertyValue)); } } /** * Creates a `$jscomp$loop$0.$jscomp$loop$prop$varName$1` replacement for a reference to * `varName`. */ private Node createLoopVarReferenceReplacement( LoopObject loopObject, Node reference, String propertyName) { Node replacement = astFactory.createGetProp( createLoopObjectNameNode(loopObject), propertyName, type(reference)); replacement.srcrefTree(reference); return replacement; } /** * Gets a name other than the loop object name to use for parameter names of the new wrapper * functions being added in the loop. * *

For a loop object "$jscomp$loop$0", this creates a unique parameter name like * "$jscomp$loop$0$jscomp$loop_param$1". */ private Node createUniqueParameterNameToUseWithinLoop(Node node, LoopObject loopObject) { return astFactory.createName( loopObject.name + LOOP_PARAM_NAME_PREFIX + uniqueIdSupplier.getUniqueId(compiler.getInput(NodeUtil.getInputId(node))), type(StandardColors.TOP_OBJECT)); } private Node createLoopObjectNameNode(LoopObject loopObject) { return astFactory.createName(loopObject.name, type(StandardColors.TOP_OBJECT)); } private class LoopObject { /** * The name of the variable having the loop's internal variables as properties, and the label * applied to the block containing the original loop body in cases where these are needed. */ private final String name; private final Set vars = new LinkedHashSet<>(); private LoopObject(String name) { this.name = name; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy