com.google.javascript.jscomp.J2clClinitPrunerPass.java Source code

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.J2clClinitPrunerPass.java

Source

/*
 * 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<String, Node> emptiedClinitMethods = new LinkedHashMap<>();
    private final AbstractCompiler compiler;
    private final List<Node> changedScopeNodes;

    J2clClinitPrunerPass(AbstractCompiler compiler, List<Node> 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<String, Node> clinitReferences = collectClinitReferences(root);
        do {
            List<Node> 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, null).process(externs, root);
    }

    private void removeRedundantClinits(Node root, List<Node> 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<Node> 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<String, Node> 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<String, Node> collectClinitReferences(Node root) {
        final Multimap<String, Node> 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<Node> cleanEmptyClinitReferences(Multimap<String, Node> clinitReferences) {
        final List<Node> 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<String, Node> clinitReplacementEntry : emptiedClinitMethods.entrySet()) {
            String clinitName = clinitReplacementEntry.getKey();
            Node replacement = clinitReplacementEntry.getValue();

            Collection<Node> 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<Node> getNonNestedParentScopeNodes(List<Node> 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<HierarchicalSet<String>> stateStack = new ArrayDeque<>();
        private HierarchicalSet<String> 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.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();
            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<T> {
        private final Set<T> currentSet = new HashSet<>();
        @Nullable
        private final HierarchicalSet<T> parent;

        public HierarchicalSet(@Nullable HierarchicalSet<T> 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.
     *
     * <p>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.
     *
     * <p>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);
    }
}