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

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

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2017 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;

/** Collects global variable references for use by {@link CrossChunkCodeMotion}. */
public final class CrossChunkReferenceCollector implements ScopedCallback, CompilerPass {

  /** Maps global variable name to the corresponding {@link Var} object. */
  private final Map varsByName = new HashMap<>();

  /**
   * Maps a given variable to a collection of references to that name. Note that
   * Var objects are not stable across multiple traversals (unlike scope root or
   * name).
   */
  private final Map referenceMap =
       new LinkedHashMap<>();

  /** The stack of basic blocks and scopes the current traversal is in. */
  private final List blockStack = new ArrayList<>();

  /** List of all top-level statements in the order they appear in the AST. */
  private final List topLevelStatements = new ArrayList<>();

  private final ScopeCreator scopeCreator;

  /**
   * JavaScript compiler to use in traversing.
   */
  private final AbstractCompiler compiler;

  private int statementCounter = 0;
  private TopLevelStatementDraft topLevelStatementDraft = null;

  /** Constructor initializes block stack. */
  CrossChunkReferenceCollector(AbstractCompiler compiler, ScopeCreator creator) {
    this.compiler = compiler;
    this.scopeCreator = creator;
  }

  /**
   * Convenience method for running this pass over a tree with this
   * class as a callback.
   */
  @Override
  public void process(Node externs, Node root) {
    checkState(topLevelStatements.isEmpty(), "process() called more than once");
    NodeTraversal.builder()
        .setCompiler(compiler)
        .setCallback(this)
        .setScopeCreator(scopeCreator)
        .traverseRoots(externs, root);
  }

  public void process(Node root) {
    checkState(topLevelStatements.isEmpty(), "process() called more than once");
    NodeTraversal.builder()
        .setCompiler(compiler)
        .setCallback(this)
        .setScopeCreator(scopeCreator)
        .traverse(root);
  }

  /**
   * Gets the variables that were referenced in this callback.
   */
  Iterable getAllSymbols() {
    return referenceMap.keySet();
  }

  /**
   * Gets the reference collection for the given variable.
   */
  ReferenceCollection getReferences(Var v) {
    return referenceMap.get(v);
  }

  ImmutableMap getGlobalVariableNamesMap() {
    return ImmutableMap.copyOf(varsByName);
  }

  /**
   * For each node, update the block stack and reference collection
   * as appropriate.
   */
  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (topLevelStatementDraft != null) {
      if (n.equals(topLevelStatementDraft.statementNode)) {
        topLevelStatements.add(new TopLevelStatement(topLevelStatementDraft));
        topLevelStatementDraft = null;
      } else if (n.isName()) {
        String varName = n.getString();
        Var v = t.getScope().getVar(varName);

        if (v != null) {
          // Only global, non-exported names can be moved
          if (v.isGlobal()
              && !compiler.getCodingConvention().isExported(v.getName(), /* local */ false)) {
            if (varsByName.containsKey(varName)) {
              checkState(Objects.equals(varsByName.get(varName), v));
            } else {
              varsByName.put(varName, v);
            }
            Reference reference = new Reference(n, t, peek(blockStack));
            if (reference.getNode() == topLevelStatementDraft.declaredNameNode) {
              topLevelStatementDraft.declaredNameReference = reference;
            } else {
              topLevelStatementDraft.nonDeclarationReferences.add(reference);
            }
            addReferenceToCollection(v, reference);
          }
        }
      }
    }
    if (isBlockBoundary(n, parent)) {
      pop(blockStack);
    }
  }

  /**
   * Updates block stack and invokes any additional behavior.
   */
  @Override
  public void enterScope(NodeTraversal t) {
    Node n = t.getScopeRoot();
    BasicBlock parent = blockStack.isEmpty() ? null : peek(blockStack);
    // Don't add all ES6 scope roots to blockStack, only those that are also scopes according to
    // the ES5 scoping rules. Other nodes that ought to be considered the root of a BasicBlock
    // are added in shouldTraverse() and removed in visit().
    if (t.isHoistScope()) {
      blockStack.add(new BasicBlock(parent, n));
    }
  }

  /**
   * Updates block stack and invokes any additional behavior.
   */
  @Override
  public void exitScope(NodeTraversal t) {
    if (t.isHoistScope()) {
      pop(blockStack);
    }
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    if (parent != null && NodeUtil.isTopLevel(parent)) {
      checkState(topLevelStatementDraft == null, n);
      topLevelStatementDraft = initializeDraftStatement(t.getChunk(), n);
    }
    // If node is a new basic block, put on basic block stack
    if (isBlockBoundary(n, parent)) {
      blockStack.add(new BasicBlock(peek(blockStack), n));
    }
    return true;
  }

  private TopLevelStatementDraft initializeDraftStatement(JSChunk module, Node statementNode) {
    TopLevelStatementDraft draft =
        new TopLevelStatementDraft(statementCounter++, module, statementNode);
    // Determine whether this statement declares a name or not.
    // If so, save its name node and value node, if any.
    if (NodeUtil.isNameDeclaration(statementNode)) {
      // variable declaration
      draft.declaredNameNode = statementNode.getFirstChild();
      draft.declaredValueNode = statementNode.getFirstFirstChild();
    } else if (statementNode.isClass()) {
      draft.declaredNameNode = statementNode.getFirstChild();
      draft.declaredValueNode = statementNode;
    } else if (statementNode.isFunction()) {
      // function declaration
      draft.declaredNameNode = statementNode.getFirstChild();
      draft.declaredValueNode = statementNode;
    } else if (statementNode.isExprResult()) {
      Node expr = checkNotNull(statementNode.getFirstChild());
      if (expr.isAssign()) {
        Node lhs = checkNotNull(expr.getFirstChild());
        Node rhs = checkNotNull(expr.getSecondChild());
        if (lhs.isName()) {
          // `varName = value;`
          draft.declaredNameNode = lhs;
          draft.declaredValueNode = rhs;
        } else if (lhs.isGetProp()) {
          Node nameNode = checkNotNull(lhs.getFirstChild());
          while (nameNode.isGetProp()) {
            nameNode = checkNotNull(nameNode.getFirstChild());
          }
          if (nameNode.isName()) {
            // `varName.some.property = value;`
            draft.declaredNameNode = nameNode;
            draft.declaredValueNode = rhs;
          }
        }
      } else if (expr.isCall()) {
        Node nameNode = null;
        Node valueNode = null;
        CodingConvention.SubclassRelationship relationship =
            compiler.getCodingConvention().getClassesDefinedByCall(expr);
        if (relationship != null) {
          // Check for $jscomp.inherits(SubC, SuperC), goog.inherits(Sub, SuperC), etc.
          String declaredName = checkNotNull(relationship.subclassName);
          for (Node callArg = expr.getSecondChild(); callArg != null; callArg = callArg.getNext()) {
            // We're assuming that the child class must be an argument to the function that
            // establishes its inheritance, which is true for `goog.inherits()` and
            // `$jscomp.inherits()`
            // TODO(bradfordcsmith): handle cases like `goog.inherits(x.ChildClass, SuperClass)`
            if (callArg.isName() && declaredName.equals(callArg.getString())) {
              nameNode = callArg;
              break;
            }
          }
        } else if (NodeUtil.isObjectDefinePropertiesDefinition(expr)) {
          // Check for $jscomp$global.Object.defineProperties.
          Node targetObject = expr.getSecondChild();

          // Get the global var being referenced in the first parameter of Object.defineProperties.
          // Can be 'Foo' or 'Foo.prototype'.
          nameNode = targetObject.isName() ? targetObject : targetObject.getFirstChild();

          // Second parameter of defineProperties is the value node.
          valueNode = targetObject.getNext();
        }

        if (nameNode != null) {
          draft.declaredNameNode = nameNode;
          draft.declaredValueNode = valueNode;
        }
      }
    }
    return draft;
  }

  private static  T pop(List list) {
    return list.remove(list.size() - 1);
  }

  private static  T peek(List list) {
    return Iterables.getLast(list);
  }

  /**
   * @return true if this node marks the start of a new basic block
   */
  private static boolean isBlockBoundary(Node n, Node parent) {
    if (parent != null) {
      switch (parent.getToken()) {
        case DO:
        case FOR:
        case FOR_IN:
        case FOR_OF:
        case FOR_AWAIT_OF:
        case TRY:
        case WHILE:
        case WITH:
        case CLASS:
          // NOTE: TRY has up to 3 child blocks:
          // TRY
          //   BLOCK
          //   BLOCK
          //     CATCH
          //   BLOCK
          // Note that there is an explicit CATCH token but no explicit
          // FINALLY token. For simplicity, we consider each BLOCK
          // a separate basic BLOCK.
          return true;
        case AND:
        case HOOK:
        case IF:
        case OR:
        case SWITCH:
        case COALESCE:
          // The first child of a conditional is not a boundary,
          // but all the rest of the children are.
          return n != parent.getFirstChild();

        default:
          break;
      }
    }

    return n.isCase();
  }

  private void addReferenceToCollection(Var v, Reference reference) {
    // Create collection if none already
    ReferenceCollection referenceInfo =
        referenceMap.computeIfAbsent(v, (Var k) -> new ReferenceCollection());

    // Add this particular reference
    referenceInfo.add(reference);
  }

  List getTopLevelStatements() {
    return Collections.unmodifiableList(topLevelStatements);
  }

  /** Determines whether the given value is eligible to be moved across modules. */
  private boolean canMoveValue(Scope scope, Node valueNode) {
    // the value is only movable if it's
    // - nothing,
    // - a constant literal,
    // - a function
    // - class without computed properties, or whose computed properties expressions are moveable
    // - an array/object literal of movable values
    // - a template literal with movable substitutions
    // - a function stub generated by CrossChunkMethodMotion.
    if (valueNode == null || NodeUtil.isLiteralValue(valueNode, true) || valueNode.isFunction()) {
      return true;
    }

    // Consider the node eligible for move if the current statement or expression is annotated
    // with @pureOrBreakMyCode.
    JSDocInfo jsDocInfo = NodeUtil.getBestJSDocInfo(valueNode);
    if (jsDocInfo != null && jsDocInfo.isPureOrBreakMyCode()) {
      return true;
    }

    switch (valueNode.getToken()) {
      case CLASS:
        // NOTE: Why aren't class extends expressions checked here?
        Node classMembers = valueNode.getLastChild();
        for (Node member = classMembers.getFirstChild();
            member != null;
            member = member.getNext()) {
          if (member.isComputedProp()) {
            Node keyExpr = member.getFirstChild();
            Node method = member.getLastChild();
            checkState(method.isFunction(), method);
            if (!canMoveValue(scope, keyExpr)) {
              return false;
            }
          } else if (member.isComputedFieldDef()) {
            Node keyExpr = member.getFirstChild();
            if (!canMoveValue(scope, keyExpr)) {
              return false;
            }
          } else if (member.isMemberFieldDef()) {
            if (member.isStaticMember() && !canMoveValue(scope, member.getFirstChild())) {
              // Unlike non-static fields,the RHS of a static field is executed at
              // class definition time. So, we must check canMoveValue for static fields.
              return false;
            }
          } else {
            checkState(member.isMemberFunctionDef() || NodeUtil.isGetOrSetKey(member), member);
          }
        }
        return true;

      case CALL:
        // In general it is not safe to move function calls, but we carve out an exception
        // for the special stub method calls used for CrossChunkMethodMotion.
        // Case: `JSCompiler_stubMethod(x)`
        Node functionName = checkNotNull(valueNode.getFirstChild());
        return functionName.isName()
            && functionName.getString().equals(CrossChunkMethodMotion.STUB_METHOD_NAME);

      case ARRAYLIT:
        // Movable if all of the array values are movable.
        for (Node child = valueNode.getFirstChild(); child != null; child = child.getNext()) {
          if (!canMoveValue(scope, child)) {
            return false;
          }
        }

        return true;

      case OBJECTLIT:
        // Movable if all of the keys and values are movable.
        for (Node child = valueNode.getFirstChild(); child != null; child = child.getNext()) {
          switch (child.getToken()) {
            case MEMBER_FUNCTION_DEF:
            case GETTER_DEF:
            case SETTER_DEF:
              break;

            case COMPUTED_PROP:
              if (!canMoveValue(scope, child.getFirstChild())
                  || !canMoveValue(scope, child.getLastChild())) {
                return false;
              }
              break;

            case STRING_KEY:
            case OBJECT_SPREAD:
              if (!canMoveValue(scope, child.getOnlyChild())) {
                return false;
              }
              break;

            default:
              throw new IllegalStateException(
                  "Unexpected child of OBJECTLIT: " + child.toStringTree());
          }
        }

        return true;

      case NAME:
        // If the value is guaranteed to never be changed after
        // this reference, then we can move it.
        Var v = scope.getVar(valueNode.getString());
        if (v != null && v.isGlobal()) {
          ReferenceCollection refCollection = getReferences(v);
          if (refCollection != null
              && refCollection.isWellDefined()
              && refCollection.isAssignedOnceInLifetime()) {
            return true;
          }
        }
        break;

      case TEMPLATELIT:
        // A template literal is movable if all of the substitutions it contains are movable.
        for (Node child = valueNode.getFirstChild(); child != null; child = child.getNext()) {
          if (child.isTemplateLitSub()) {
            if (!canMoveValue(scope, child.getFirstChild())) {
              return false;
            }
          } else {
            checkState(child.isTemplateLitString(), child);
          }
        }
        return true;

      default:
        break;
    }

    return false;
  }

  /** Represents a top-level statement and the references to global names it contains. */
  final class TopLevelStatement {

    /** 0-based index indicating original order of this statement in the source. */
    private final int originalOrder;

    private final JSChunk module;
    private final Node statementNode;
    private final List nonDeclarationReferences;
    private final Reference declaredNameReference;
    private final Node declaredValueNode;

    TopLevelStatement(TopLevelStatementDraft draft) {
      this.originalOrder = draft.originalOrder;
      this.module = draft.module;
      this.statementNode = draft.statementNode;
      this.nonDeclarationReferences = Collections.unmodifiableList(draft.nonDeclarationReferences);
      this.declaredNameReference = draft.declaredNameReference;
      this.declaredValueNode = draft.declaredValueNode;
    }

    int getOriginalOrder() {
      return originalOrder;
    }

    JSChunk getModule() {
      return module;
    }

    Node getStatementNode() {
      return statementNode;
    }

    List getNonDeclarationReferences() {
      return Collections.unmodifiableList(nonDeclarationReferences);
    }

    boolean isDeclarationStatement() {
      return declaredNameReference != null;
    }

    Reference getDeclaredNameReference() {
      return checkNotNull(declaredNameReference);
    }

    @Nullable
    Node getDeclaredValueNode() {
      return declaredValueNode;
    }

    boolean isMovableDeclaration() {
      return isDeclarationStatement()
          && canMoveValue(declaredNameReference.getScope(), declaredValueNode);
    }
  }

  /** Holds statement info temporarily while the statement is being traversed. */
  private static final class TopLevelStatementDraft {

    /** 0-based index indicating original order of this statement in the source. */
    final int originalOrder;

    final JSChunk module;
    final Node statementNode;
    final List nonDeclarationReferences = new ArrayList<>();
    Node declaredValueNode = null;
    Node declaredNameNode = null;
    Reference declaredNameReference = null;

    TopLevelStatementDraft(int originalOrder, JSChunk module, Node statementNode) {
      this.originalOrder = originalOrder;
      this.module = module;
      this.statementNode = statementNode;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy