Package com.google.javascript.jscomp

Source Code of com.google.javascript.jscomp.Es6RewriteLetConst

/*
* 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 com.google.common.collect.HashMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.Normalize.NormalizeStatements;
import com.google.javascript.jscomp.Scope.Var;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

/**
* Rewrites "let"s and "const"s as "var"s, renaming declarations when necessary.
*
* @author moz@google.com (Michael Zhou)
*/
public class Es6RewriteLetConst extends AbstractPostOrderCallback
    implements HotSwapCompilerPass {

  private final AbstractCompiler compiler;
  private final Map<Node, Map<String, String>> renameMap = new LinkedHashMap<>();
  private final Set<Node> blockScopedDeclarations = new HashSet<>();
  private final Set<String> undeclaredNames = new HashSet<>();

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

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    // TODO(moz): Add support for renaming classes.
    if (!n.isLet() && !n.isConst() && !NodeUtil.isBlockScopedFunctionDeclaration(n)) {
      return;
    }

    Scope scope = t.getScope();
    Node nameNode = n.getFirstChild();
    if (!n.isFunction() && !nameNode.hasChildren()
        && (parent == null || !NodeUtil.isEnhancedFor(parent))) {
      nameNode.addChildToFront(
          IR.name("undefined").useSourceInfoIfMissingFrom(nameNode));
    }

    String oldName = nameNode.getString();
    if (n.isLet() || n.isConst()) {
      blockScopedDeclarations.add(n);
    }
    Scope hoistScope = scope.getClosestHoistScope();
    boolean doRename = false;
    if (scope != hoistScope) {
      doRename = hoistScope.isDeclared(oldName, true)
          || undeclaredNames.contains(oldName);
      String newName = doRename
          ? oldName + "$" + compiler.getUniqueNameIdSupplier().get()
          : oldName;
      Var oldVar = scope.getVar(oldName);
      scope.undeclare(oldVar);
      hoistScope.declare(newName, nameNode, null, oldVar.input);
      if (doRename) {
        nameNode.setString(newName);
        Node scopeRoot = scope.getRootNode();
        if (!renameMap.containsKey(scopeRoot)) {
          renameMap.put(scopeRoot, new HashMap<String, String>());
        }

        renameMap.get(scopeRoot).put(oldName, newName);
      }
    }
    if (doRename) {
      t.getCompiler().reportCodeChange();
    }
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverseRoots(
        compiler, Lists.newArrayList(externs, root), new CollectUndeclaredNames());
    NodeTraversal.traverseRoots(compiler, Lists.newArrayList(externs, root), this);
    NodeTraversal.traverseRoots(
        compiler, Lists.newArrayList(externs, root), new RenameReferences());

    LoopClosureTransformer transformer = new LoopClosureTransformer();
    NodeTraversal.traverseRoots(
        compiler, Lists.newArrayList(externs, root), transformer);
    transformer.transformLoopClosure();
    varify();
    NodeTraversal.traverseRoots(compiler, Lists.newArrayList(externs, root),
        new RewriteBlockScopedFunctionDeclaration());
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, new CollectUndeclaredNames());
    NodeTraversal.traverse(compiler, scriptRoot, this);
    NodeTraversal.traverse(compiler, scriptRoot, new RenameReferences());

    LoopClosureTransformer transformer = new LoopClosureTransformer();
    NodeTraversal.traverse(compiler, scriptRoot, transformer);
    transformer.transformLoopClosure();
    varify();
    NodeTraversal.traverse(compiler, scriptRoot, new RewriteBlockScopedFunctionDeclaration());
  }

  private void varify() {
    if (!blockScopedDeclarations.isEmpty()) {
      for (Node n : blockScopedDeclarations) {
        if (n.isConst()) {
          JSDocInfoBuilder builder = (n.getJSDocInfo() == null)
              ? new JSDocInfoBuilder(true)
              : JSDocInfoBuilder.copyFrom(n.getJSDocInfo());
          builder.recordConstancy();
          JSDocInfo info = builder.build(n);
          info.setAssociatedNode(n);
          n.setJSDocInfo(info);
        }
        n.setType(Token.VAR);
      }
      compiler.reportCodeChange();
    }
  }

  private class RewriteBlockScopedFunctionDeclaration extends
      AbstractPostOrderCallback {

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isFunction()) {
        if (NormalizeStatements.maybeNormalizeFunctionDeclaration(n)) {
          compiler.reportCodeChange();
        }
      }
    }
  }

  /**
   * Record undeclared names and aggressively rename possible references to them.
   * Eg: In "{ let inner; } use(inner);", we rename the let declared variable.
   */
  private class CollectUndeclaredNames extends AbstractPostOrderCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isName() && !t.getScope().isDeclared(n.getString(), true)) {
        undeclaredNames.add(n.getString());
      }
    }
  }

  /**
   * Renames references when necessary.
   */
  private class RenameReferences extends AbstractPostOrderCallback {

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!NodeUtil.isReferenceName(n)) {
        return;
      }

      Scope referencedIn = t.getScope();
      String oldName = n.getString();
      Scope current = referencedIn;
      boolean doRename = false;
      String newName = null;
      while (current != null) {
        Map<String, String> renamesAtCurrentLevel = renameMap.get(current.getRootNode());
        if (current.isDeclared(oldName, false)) {
          return;
        } else if (renamesAtCurrentLevel != null
            && renamesAtCurrentLevel.containsKey(oldName)) {
          doRename = true;
          newName = renamesAtCurrentLevel.get(oldName);
          break;
        } else {
          current = current.getParent();
        }
      }
      if (doRename) {
        n.setString(newName);
        t.getCompiler().reportCodeChange();
      }
    }
  }

  /**
   * Transforms let/const declarations captured by loop closures.
   */
  private class LoopClosureTransformer extends AbstractPostOrderCallback {
    private static final String LOOP_OBJECT_NAME = "$jscomp$loop";

    private final Map<Node, LoopObject> loopObjectMap = new LinkedHashMap<>();
    private final Multimap<Node, LoopObject> functionLoopObjectsMap =
        LinkedHashMultimap.create();
    private final Multimap<Node, String> functionHandledMap = HashMultimap.create();
    private final Multimap<Var, Node> referenceMap = LinkedHashMultimap.create();

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!NodeUtil.isReferenceName(n)) {
        return;
      }

      String name = n.getString();
      Scope referencedIn = t.getScope();
      Var var = referencedIn.getVar(name);
      if (var == null) {
        return;
      }

      if (!var.isLet() && !var.isConst()) {
        return;
      }

      // 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(s.getRootNode())) {
          loopNode = scopeRoot;
          break;
        } else if (scopeRoot.getParent() != null
            && NodeUtil.isLoopStructure(scopeRoot.getParent())) {
          loopNode = scopeRoot.getParent();
          break;
        } else if (s.isFunctionBlockScope() || s.isGlobal()) {
          return;
        }
      }

      referenceMap.put(var, n);

      // Traverse scopes from reference scope to declaration scope.
      // If we hit a function - loop closure detected.
      for (Scope s = referencedIn; s != declaredIn; s = s.getParent()) {
        if (s.isFunctionBlockScope()) {
          Node function = s.getRootNode().getParent();
          if (functionHandledMap.get(function).contains(name)) {
            return;
          }
          functionHandledMap.put(function, name);

          if (!loopObjectMap.containsKey(loopNode)) {
            loopObjectMap.put(loopNode,
                new LoopObject(
                    LOOP_OBJECT_NAME + "$" + compiler.getUniqueNameIdSupplier().get()));
          }
          LoopObject object = loopObjectMap.get(loopNode);
          object.vars.add(var);

          functionLoopObjectsMap.put(function,  object);
          return;
        }
      }
    }

    private void transformLoopClosure() {
      if (loopObjectMap.isEmpty()) {
        return;
      }

      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 = {i: 0, j: $jscomp$loop$0.i}
        // They are initialized lazily by changing declarations into assignments
        // later.
        LoopObject object = loopObjectMap.get(loopNode);
        Node objectLit = IR.objectlit();
        Node objectLitNextIteration = IR.objectlit();
        for (Var var : object.vars) {
          objectLit.addChildToBack(IR.stringKey(var.name, IR.name("undefined")));
          objectLitNextIteration.addChildToBack(
              IR.stringKey(var.name, IR.getprop(IR.name(object.name),
              IR.string(var.name))));
        }

        Node updateLoopObject = IR.assign(IR.name(object.name), objectLitNextIteration);
        loopNode.getParent().addChildBefore(
            IR.var(IR.name(object.name), objectLit)
                .useSourceInfoIfMissingFromForTree(loopNode),
            loopNode);
        if (NodeUtil.isVanillaFor(loopNode)) { // For
          // The initializer is pulled out and placed prior to the loop.
          Node initializer = loopNode.getFirstChild();
          loopNode.replaceChild(initializer, IR.empty());
          if (!initializer.isEmpty()) {
            loopNode.getParent().addChildBefore(initializer, loopNode);
          }
          Node increment = loopNode.getChildAtIndex(2);
          if (increment.isEmpty()) {
            loopNode.replaceChild(
                increment,
                updateLoopObject.useSourceInfoIfMissingFromForTree(loopNode));
          } else {
            Node placeHolder = IR.empty();
            loopNode.replaceChild(increment, placeHolder);
            loopNode.replaceChild(
                placeHolder,
                IR.comma(updateLoopObject, increment)
                    .useSourceInfoIfMissingFromForTree(loopNode));
          }
        } else if (loopNode.isDo()) { // do-while, put at the end of the block
          loopNode.getFirstChild().addChildToBack(IR.exprResult(updateLoopObject)
              .useSourceInfoIfMissingFromForTree(loopNode));
        } else { // For-in, for-of or while, put at the end of the block
          loopNode.getLastChild().addChildToBack(IR.exprResult(updateLoopObject)
              .useSourceInfoIfMissingFromForTree(loopNode));
        }

        // For captured variables, change declarations to assignments on the
        // corresponding field of the introduced object. Rename all references
        // accordingly.
        for (Var var : object.vars) {
          for (Node reference : referenceMap.get(var)) {
            // For-of and for-in declarations are not altered, since they are
            // used as temporary variables for assignment.
            if (NodeUtil.isEnhancedFor(loopNode)
                && loopNode.getFirstChild() == reference.getParent()) {
              loopNode.getLastChild().addChildToFront(
                  IR.exprResult(IR.assign(
                      IR.getprop(IR.name(object.name), IR.string(var.name)),
                      IR.name(var.name)))
                      .useSourceInfoIfMissingFromForTree(reference));
            } else {
              if (NodeUtil.isNameDeclaration(reference.getParent())) {
                Node declaration = reference.getParent();
                Node grandParent = declaration.getParent();
                // Normalize: "let i = 0, j = 0;" becomes "let i = 0; let j = 0;"
                while (declaration.getChildCount() > 1) {
                  Node name = declaration.getLastChild();
                  grandParent.addChildAfter(
                      IR.declaration(
                          name.detachFromParent(), declaration.getType())
                          .useSourceInfoIfMissingFromForTree(declaration),
                      declaration);
                }

                // Change declaration to assignment, or just drop it if there's
                // no initial value.
                if (reference.hasChildren()) {
                  declaration = reference.getParent(); // Might have changed now
                  Node newReference = IR.name(var.name);
                  Node replacement = IR.exprResult(
                      IR.assign(newReference, reference.removeFirstChild()))
                          .useSourceInfoIfMissingFromForTree(declaration);
                  grandParent.replaceChild(declaration, replacement);
                  reference = newReference;
                } else {
                  grandParent.removeChild(declaration);
                }
              }

              // Change reference to GETPROP.
              reference.getParent().replaceChild(
                  reference,
                  IR.getprop(IR.name(object.name), IR.string(var.name))
                      .useSourceInfoIfMissingFromForTree(reference));
            }
          }
        }
      }

      // Create wrapper functions and call them.
      for (Node function : functionLoopObjectsMap.keySet()) {
        Node returnNode = IR.returnNode();
        Collection<LoopObject> objects = functionLoopObjectsMap.get(function);
        Node[] objectNames = new Node[objects.size()];
        Node[] objectNamesForCall = new Node[objects.size()];
        int i = 0;
        for (LoopObject object : objects) {
          objectNames[i] = IR.name(object.name);
          objectNamesForCall[i] = IR.name(object.name);
          i++;
        }

        Node iife = IR.function(
            IR.name(""), IR.paramList(objectNames), IR.block(returnNode));
        Node call = IR.call(iife, objectNamesForCall);
        call.putBooleanProp(Node.FREE_CALL, true);
        function.getParent().replaceChild(
            function, call.useSourceInfoIfMissingFromForTree(function));
        returnNode.addChildToFront(function);
      }
    }

    private class LoopObject {
      private final String name;
      private final Set<Var> vars = new LinkedHashSet<>();

      private LoopObject(String name) {
        this.name = name;
      }
    }
  }
}
TOP

Related Classes of com.google.javascript.jscomp.Es6RewriteLetConst

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.