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

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

/*
 * Copyright 2016 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.NodeTraversal.ChangeScopeRootCallback;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;

/** An optimization pass to prune J2CL clinits. */
public class J2clClinitPrunerPass implements CompilerPass {

  private final Map emptiedClinitMethods = new LinkedHashMap<>();
  private final AbstractCompiler compiler;
  private final List changedScopeNodes;

  J2clClinitPrunerPass(AbstractCompiler compiler, List changedScopeNodes) {
    this.compiler = compiler;
    this.changedScopeNodes = changedScopeNodes;
  }

  @Override
  public void process(Node externs, Node root) {
    if (!J2clSourceFileChecker.shouldRunJ2clPasses(compiler)) {
      return;
    }

    removeRedundantClinits(root, changedScopeNodes);

    pruneEmptyClinits(root, changedScopeNodes);

    if (emptiedClinitMethods.isEmpty()) {
      // Since no clinits are pruned, we don't need look for more opportunities.
      return;
    }

    Multimap clinitReferences = collectClinitReferences(root);
    do {
      List newChangedScopes = cleanEmptyClinitReferences(clinitReferences);
      pruneEmptyClinits(root, newChangedScopes);
    } while (!emptiedClinitMethods.isEmpty());

    // Update the function side-effect markers on the AST.
    // Removing a clinit from a function may make it side-effect free.
    new PureFunctionIdentifier.Driver(compiler).process(externs, root);
  }

  private void removeRedundantClinits(Node root, List changedScopeNodes) {
    RedundantClinitPruner redundantClinitPruner = new RedundantClinitPruner();
    NodeTraversal.traverseScopeRoots(
        compiler,
        root,
        getNonNestedParentScopeNodes(changedScopeNodes),
        redundantClinitPruner,
        redundantClinitPruner, // FunctionCallback
        true);

    NodeTraversal.traverseScopeRoots(
        compiler, root, changedScopeNodes, new LookAheadRedundantClinitPruner(), false);
  }

  private void pruneEmptyClinits(Node root, List changedScopes) {
    // Clear emptiedClinitMethods before EmptyClinitPruner to populate only with new ones.
    emptiedClinitMethods.clear();
    NodeTraversal.traverseScopeRoots(
        compiler, root, changedScopes, new EmptyClinitPruner(), false);

    // Make sure replacements are to final destination instead of pointing intermediate ones.
    for (Entry clinitReplacementEntry : emptiedClinitMethods.entrySet()) {
      clinitReplacementEntry.setValue(resolveReplacement(clinitReplacementEntry.getValue()));
    }
  }

  private Node resolveReplacement(Node node) {
    if (node == null) {
      return null;
    }
    String clinitName = getClinitMethodName(node);
    return emptiedClinitMethods.containsKey(clinitName)
        ? resolveReplacement(emptiedClinitMethods.get(clinitName))
        : node;
  }

  private Multimap collectClinitReferences(Node root) {
    final Multimap clinitReferences = HashMultimap.create();
    NodeTraversal.traverse(
        compiler,
        root,
        new AbstractPostOrderCallback() {
          @Override
          public void visit(NodeTraversal t, Node node, Node parent) {
            String clinitName = getClinitMethodName(node);
            if (clinitName != null) {
              clinitReferences.put(clinitName, node);
            }
          }
        });
    return clinitReferences;
  }

  private List cleanEmptyClinitReferences(Multimap clinitReferences) {
    final List newChangedScopes = new ArrayList<>();
    // resolveReplacement step above should not require any particular iteration order of the
    // emptiedClinitMethods but we are using LinkedHashMap to be extra safe.
    for (Entry clinitReplacementEntry : emptiedClinitMethods.entrySet()) {
      String clinitName = clinitReplacementEntry.getKey();
      Node replacement = clinitReplacementEntry.getValue();

      Collection references = clinitReferences.removeAll(clinitName);
      for (Node reference : references) {
        Node changedScope = NodeUtil.getEnclosingChangeScopeRoot(reference.getParent());
        if (replacement == null) {
          NodeUtil.deleteFunctionCall(reference, compiler);
        } else {
          replacement = replacement.cloneTree();
          reference.replaceWith(replacement);
          compiler.reportChangeToChangeScope(changedScope);
          clinitReferences.put(getClinitMethodName(replacement), replacement);
        }
        newChangedScopes.add(changedScope);
      }
    }
    return newChangedScopes;
  }

  private static List getNonNestedParentScopeNodes(List changedScopeNodes) {
    return changedScopeNodes == null
        ? null
        : NodeUtil.removeNestedChangeScopeNodes(
            NodeUtil.getParentChangeScopeNodes(changedScopeNodes));
  }

  /** Removes redundant clinit calls inside method body if it is guaranteed to be called earlier. */
  private final class RedundantClinitPruner implements Callback, ChangeScopeRootCallback {

    @Override
    public void enterChangeScopeRoot(AbstractCompiler compiler, Node root) {
      // Reset the clinit call tracking when starting over on a new scope.
      clinitsCalledAtBranch = new HierarchicalSet<>(null);
      stateStack.clear();
    }

    private final Deque> stateStack = new ArrayDeque<>();
    private HierarchicalSet clinitsCalledAtBranch = new HierarchicalSet<>(null);

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node node, Node parent) {
      if (NodeUtil.isFunctionDeclaration(node) || node.isScript()) {
        // In the case of function declarations, unlike function expressions, we don't know when the
        // function will be executed so assume there are no clinits already executed.
        // In the case of script roots, when using modules, we can not assume the scripts are going
        // to be executed in program order.
        stateStack.addLast(clinitsCalledAtBranch);
        clinitsCalledAtBranch = new HierarchicalSet<>(null);
      } 

      if (isNewControlBranch(parent)) {
        clinitsCalledAtBranch = new HierarchicalSet<>(clinitsCalledAtBranch);
        if (isClinitMethod(parent)) {
          // Adds itself as any of your children can assume clinit is already called.
          clinitsCalledAtBranch.add(getQualifiedNameOfFunction(parent));
        }
      }

      return true;
    }

    @Override
    public void visit(NodeTraversal t, Node node, Node parent) {
      tryRemovingClinit(node);

      if (isNewControlBranch(parent)) {
        clinitsCalledAtBranch = clinitsCalledAtBranch.parent;
      }

      if (NodeUtil.isFunctionDeclaration(node) || node.isScript()) {
        // In the case of function declarations, unlike function expressions, we don't know when the
        // function will be executed so assume there are no clinits already executed.
        // In the case of script roots, when using modules, we can not assume the scripts are going
        // to be executed in program order.
        clinitsCalledAtBranch = stateStack.removeLast();
      }
    }

    private void tryRemovingClinit(Node node) {
      String clinitName = getClinitMethodName(node);
      if (clinitName == null) {
        return;
      }

      if (clinitsCalledAtBranch.add(clinitName)) {
        // This is the first time we are seeing this clinit so cannot remove it.
        return;
      }

      NodeUtil.deleteFunctionCall(node, compiler);
    }

    private boolean isNewControlBranch(Node n) {
      return n != null
          && (NodeUtil.isControlStructure(n)
              || n.isDefaultValue()
              || n.isHook()
              || n.isAnd()
              || n.isOr()
              || n.isFunction());
    }
  }

  // TODO(michaelthomas): Prune clinit calls in functions, if previous functions or the immediate
  // next function guarantees clinit call. With that we won't need this pass.
  /**
   * Prunes clinit calls which immediately precede calls to a static function which calls the same
   * clinit. e.g. "Foo.clinit(); return new Foo()" -> "return new Foo()"
   */
  private final class LookAheadRedundantClinitPruner extends AbstractPostOrderCallback {

    @Override
    public void visit(NodeTraversal t, Node node, Node parent) {
      if (!node.isExprResult()) {
        return;
      }

      // Find clinit calls.
      String clinitName = getClinitMethodName(node.getFirstChild());
      if (clinitName == null) {
        return;
      }

      // Check for calls to a static method immediately following the clinit call.
      Node callOrNewNode = getCallOrNewNode(node.getNext());
      if (callOrNewNode == null || !callOrNewNode.getFirstChild().isName()) {
        return;
      }

      // Check that the call isn't a recursive call to the same function.
      Node enclosingFunction = NodeUtil.getEnclosingFunction(node);
      if (enclosingFunction == null || callOrNewNode.getFirstChild().getString()
          .equals(NodeUtil.getNearestFunctionName(enclosingFunction))) {
        return;
      }

      // Find the definition of the function being called.
      Var var = t.getScope().getVar(callOrNewNode.getFirstChild().getString());
      if (var == null || var.getInitialValue() == null || !var.getInitialValue().isFunction()) {
        return;
      }

      // Check that the clinit call is safe to prune.
      Node staticFnNode = var.getInitialValue();
      if (callsClinit(staticFnNode, clinitName) && hasSafeArguments(t, callOrNewNode)) {
        parent.removeChild(node);
        compiler.reportChangeToEnclosingScope(parent);
      }
    }

    /**
     * Returns whether the arguments to the specified call/new node are "safe". i.e. they are only
     * parameters to the enclosing function or literal values (and not e.g. a static field reference
     * which might need the clinit we are trying to remove).
     */
    private boolean hasSafeArguments(NodeTraversal t, Node callOrNewNode) {
      Node child = callOrNewNode.getSecondChild();
      while (child != null) {
        if (!NodeUtil.isLiteralValue(child, false /* includeFunctions */)
            && !isParameter(t, child)) {
          return false;
        }
        child = child.getNext();
      }
      return true;
    }

    /** Returns whether the specified node is defined as a parameter to its enclosing function. */
    private boolean isParameter(NodeTraversal t, Node n) {
      if (!n.isName()) {
        return false;
      }
      Var var = t.getScope().getVar(n.getString());
      return var.getParentNode().isParamList();
    }

    /**
     * Returns the call node associated with the specified node if one exists, otherwise returns
     * null.
     */
    private Node getCallOrNewNode(Node n) {
      if (n == null) {
        return null;
      }
      switch (n.getToken()) {
        case EXPR_RESULT:
        case RETURN:
          return getCallOrNewNode(n.getFirstChild());
        case CALL:
        case NEW:
          return n;
        case CONST:
        case LET:
        case VAR:
          return n.hasOneChild() ? getCallOrNewNode(n.getFirstFirstChild()) : null;
        default:
          return null;
      }
    }

    /** Returns whether the specified function contains a call to the specified clinit. */
    private boolean callsClinit(Node fnNode, String clinitName) {
      checkNotNull(clinitName);
      // TODO(michaelthomas): Consider checking all children, but watch out for return statements
      // that could short-circuit the clinit.
      Node child = fnNode.getLastChild().getFirstChild();
      return child != null
          && child.isExprResult()
          && clinitName.equals(getClinitMethodName(child.getFirstChild()));
    }
  }

  /** A traversal callback that removes the body of empty clinits. */
  private final class EmptyClinitPruner extends AbstractPostOrderCallback {

    @Override
    public void visit(NodeTraversal t, Node node, Node parent) {
      if (!isClinitMethod(node)) {
        return;
      }

      trySubstituteEmptyFunction(node);
    }

    /** Clears the body of any functions that are equivalent to empty functions. */
    private void trySubstituteEmptyFunction(Node fnNode) {
      String fnQualifiedName = getQualifiedNameOfFunction(fnNode);

      // Ignore anonymous/constructor functions.
      if (Strings.isNullOrEmpty(fnQualifiedName)) {
        return;
      }

      Node body = fnNode.getLastChild();
      if (!body.hasChildren()) {
        return;
      }

      // Ensure that the first expression in the body is setting itself to the empty function
      Node firstExpr = body.getFirstChild();
      if (!isAssignToEmptyFn(firstExpr, fnQualifiedName)) {
        return;
      }

      Node secondExpr = firstExpr.getNext();
      Node replaceReferencesWith;
      if (secondExpr == null) {
        // There are no expressions in clinit so it is noop; remove all references.
        replaceReferencesWith = null;
      } else if (secondExpr.getNext() == null
          && secondExpr.isExprResult()
          && getClinitMethodName(secondExpr.getFirstChild()) != null) {
        // Only expression in clinit is a call to another clinit. We can safely replace all
        // references with the other clinit.
        replaceReferencesWith = secondExpr.getFirstChild();
      } else {
        // Clinit is not empty.
        return;
      }

      emptiedClinitMethods.put(fnQualifiedName, replaceReferencesWith);
      NodeUtil.deleteNode(firstExpr, compiler);
    }

    private boolean isAssignToEmptyFn(Node node, String enclosingFnName) {
      if (!NodeUtil.isExprAssign(node)) {
        return false;
      }

      Node lhs = node.getFirstFirstChild();
      if (lhs.isGetElem()) {
        // This can happen if code has been inserted, for example for instrumentation.
        return false;
      }
      checkState(lhs.isName() || lhs.isGetProp(), lhs);

      Node rhs = node.getFirstChild().getLastChild();
      return NodeUtil.isEmptyFunctionExpression(rhs)
          && Objects.equals(NodeUtil.getBestLValueName(lhs), enclosingFnName);
    }
  }

  private static boolean isClinitMethod(Node node) {
    return node.isFunction() && isClinitMethodName(getQualifiedNameOfFunction(node));
  }

  private static String getClinitMethodName(Node node) {
    if (node.isCall()) {
      String fnName = NodeUtil.getBestLValueName(node.getFirstChild());
      return isClinitMethodName(fnName) ? fnName : null;
    }
    return null;
  }

  private static boolean isClinitMethodName(String fnName) {
    // The '.$clinit' case) only happens when collapseProperties is off.
    return fnName != null && (fnName.endsWith("$$0clinit") || fnName.endsWith(".$clinit"));
  }

  /**
   * A minimalist implelementation of a hierarchical Set where an item might exist in current set or
   * any of its parents.
   */
  private static class HierarchicalSet {
    private final Set currentSet = new HashSet<>();
    @Nullable private final HierarchicalSet parent;

    public HierarchicalSet(@Nullable HierarchicalSet parent) {
      this.parent = parent;
    }

    public boolean add(T o) {
      return !parentsContains(o) && currentSet.add(o);
    }

    /** Returns true either my parent or any of its parents contains the item. */
    private boolean parentsContains(T o) {
      return parent != null && (parent.currentSet.contains(o) || parent.parentsContains(o));
    }
  }

  /**
   * Returns the qualifed name {@code function} is being assigned to.
   *
   * 

This implementation abstracts over the various ways of naming a function. ASSIGN need not be * involved and there may not be a sequence of GETPROPs representing the name. * *

TODO(b/123354857): Delete this method when naming concepts have been unified. It was created * as a temporary measure to support ES6 in this pass without becoming blocked on name APIs. */ private static String getQualifiedNameOfFunction(Node function) { checkArgument(function.isFunction(), function); // The node representing the name (e.g. GETPROP, MEMBER_FUNCTION_DEF, etc.). Node lValue = NodeUtil.getBestLValue(function); return NodeUtil.getBestLValueName(lValue); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy