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

Java tutorial

Introduction

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

Source

/*
 * Copyright 2008 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.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.ReferenceCollectingCallback.Behavior;
import com.google.javascript.jscomp.ReferenceCollectingCallback.Reference;
import com.google.javascript.jscomp.ReferenceCollectingCallback.ReferenceCollection;
import com.google.javascript.jscomp.ReferenceCollectingCallback.ReferenceMap;
import com.google.javascript.rhino.Node;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Using the infrastructure provided by VariableReferencePass, identify
 * variables that are used only once and in a way that is safe to move, and then
 * inline them.
 *
 * This pass has two "modes." One mode only inlines variables declared as
 * constants, for legacy compiler clients. The second mode inlines any
 * variable that we can provably inline. Note that the second mode is a
 * superset of the first mode. We only support the first mode for
 * backwards-compatibility with compiler clients that don't want
 * --inline_variables.
 *
 * The approach of this pass is similar to {@link CrossModuleCodeMotion}
 *
 * @author kushal@google.com (Kushal Dave)
 * @author nicksantos@google.com (Nick Santos)
 */
class InlineVariables implements CompilerPass {

    private final AbstractCompiler compiler;

    enum Mode {
        // Only inline things explicitly marked as constant.
        CONSTANTS_ONLY,
        // Locals only
        LOCALS_ONLY, ALL
    }

    private final Mode mode;

    // Inlines all strings, even if they increase the size of the gzipped binary.
    private final boolean inlineAllStrings;

    private final IdentifyConstants identifyConstants = new IdentifyConstants();

    InlineVariables(AbstractCompiler compiler, Mode mode, boolean inlineAllStrings) {
        this.compiler = compiler;
        this.mode = mode;
        this.inlineAllStrings = inlineAllStrings;
    }

    @Override
    public void process(Node externs, Node root) {
        ReferenceCollectingCallback callback = new ReferenceCollectingCallback(compiler, new InliningBehavior(),
                getFilterForMode());
        callback.process(externs, root);
    }

    private Predicate<Var> getFilterForMode() {
        switch (mode) {
        case ALL:
            return Predicates.alwaysTrue();
        case LOCALS_ONLY:
            return new IdentifyLocals();
        case CONSTANTS_ONLY:
            return new IdentifyConstants();
        default:
            throw new IllegalStateException();
        }
    }

    /**
     * Filters variables declared as "constant", and declares them in the outer
     * declaredConstants map.
     *
     * In Google coding conventions, this means anything declared with @const
     * or named in all caps, and initialized to an immutable value.
     * CheckConsts has already verified that these are truly constants.
     */
    private static class IdentifyConstants implements Predicate<Var> {
        @Override
        public boolean apply(Var var) {
            return var.isInferredConst();
        }
    }

    /**
     * Filters non-global variables.
     */
    private static class IdentifyLocals implements Predicate<Var> {
        @Override
        public boolean apply(Var var) {
            return var.scope.isLocal();
        }
    }

    private static class AliasCandidate {
        private final Var alias;
        private final ReferenceCollection refInfo;

        AliasCandidate(Var alias, ReferenceCollection refInfo) {
            this.alias = alias;
            this.refInfo = refInfo;
        }
    }

    /**
     * Builds up information about nodes in each scope. When exiting the
     * scope, inspects all variables in that scope, and inlines any
     * that we can.
     */
    private class InliningBehavior implements Behavior {

        /**
         * A list of variables that should not be inlined, because their
         * reference information is out of sync with the state of the AST.
         */
        private final Set<Var> staleVars = new HashSet<>();

        /**
         * Stored possible aliases of variables that never change, with
         * all the reference info about those variables. Hashed by the NAME
         * node of the variable being aliased.
         */
        final Map<Node, AliasCandidate> aliasCandidates = new HashMap<>();

        @Override
        public void afterExitScope(NodeTraversal t, ReferenceMap referenceMap) {
            collectAliasCandidates(t, referenceMap);
            doInlinesForScope(t, referenceMap);
        }

        /**
         * If any of the variables are well-defined and alias other variables,
         * mark them as aliasing candidates.
         */
        private void collectAliasCandidates(NodeTraversal t, ReferenceMap referenceMap) {
            if (mode != Mode.CONSTANTS_ONLY) {
                for (Iterator<Var> it = t.getScope().getVars(); it.hasNext();) {
                    Var v = it.next();
                    ReferenceCollection referenceInfo = referenceMap.getReferences(v);

                    // NOTE(nicksantos): Don't handle variables that are never used.
                    // The tests are much easier to write if you don't, and there's
                    // another pass that handles unused variables much more elegantly.
                    if (referenceInfo != null && referenceInfo.references.size() >= 2
                            && referenceInfo.isWellDefined() && referenceInfo.isAssignedOnceInLifetime()) {
                        Reference init = referenceInfo.getInitializingReference();
                        Node value = init.getAssignedValue();
                        if (value != null && value.isName() && !value.getString().equals(v.getName())) {
                            aliasCandidates.put(value, new AliasCandidate(v, referenceInfo));
                        }
                    }
                }
            }
        }

        /**
         * For all variables in this scope, see if they are only used once.
         * If it looks safe to do so, inline them.
         */
        private void doInlinesForScope(NodeTraversal t, ReferenceMap referenceMap) {

            boolean maybeModifiedArguments = maybeEscapedOrModifiedArguments(t.getScope(), referenceMap);
            for (Iterator<Var> it = t.getScope().getVars(); it.hasNext();) {
                Var v = it.next();

                ReferenceCollection referenceInfo = referenceMap.getReferences(v);

                // referenceInfo will be null if we're in constants-only mode
                // and the variable is not a constant.
                if (referenceInfo == null || isVarInlineForbidden(v)) {
                    // Never try to inline exported variables or variables that
                    // were not collected or variables that have already been inlined.
                    continue;
                } else if (isInlineableDeclaredConstant(v, referenceInfo)) {
                    Reference init = referenceInfo.getInitializingReferenceForConstants();
                    Node value = init.getAssignedValue();
                    inlineDeclaredConstant(v, value, referenceInfo.references);
                    staleVars.add(v);
                } else if (mode == Mode.CONSTANTS_ONLY) {
                    // If we're in constants-only mode, don't run more aggressive
                    // inlining heuristics. See InlineConstantsTest.
                    continue;
                } else {
                    inlineNonConstants(v, referenceInfo, maybeModifiedArguments);
                }
            }
        }

        private boolean maybeEscapedOrModifiedArguments(Scope scope, ReferenceMap referenceMap) {
            if (scope.isLocal()) {
                Var arguments = scope.getArgumentsVar();
                ReferenceCollection refs = referenceMap.getReferences(arguments);
                if (refs != null && !refs.references.isEmpty()) {
                    for (Reference ref : refs.references) {
                        Node refNode = ref.getNode();
                        Node refParent = ref.getParent();
                        // Any reference that is not a read of the arguments property
                        // consider a escape of the arguments object.
                        if (!(NodeUtil.isGet(refParent) && refNode == ref.getParent().getFirstChild()
                                && !isLValue(refParent))) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

        private boolean isLValue(Node n) {
            Node parent = n.getParent();
            return (parent.isInc() || parent.isDec()
                    || (NodeUtil.isAssignmentOp(parent) && parent.getFirstChild() == n));
        }

        private void inlineNonConstants(Var v, ReferenceCollection referenceInfo, boolean maybeModifiedArguments) {
            int refCount = referenceInfo.references.size();
            Reference declaration = referenceInfo.references.get(0);
            Reference init = referenceInfo.getInitializingReference();
            int firstRefAfterInit = (declaration == init) ? 2 : 3;

            if (refCount > 1 && isImmutableAndWellDefinedVariable(v, referenceInfo)) {
                // if the variable is referenced more than once, we can only
                // inline it if it's immutable and never defined before referenced.
                Node value;
                if (init != null) {
                    value = init.getAssignedValue();
                } else {
                    // Create a new node for variable that is never initialized.
                    Node srcLocation = declaration.getNode();
                    value = NodeUtil.newUndefinedNode(srcLocation);
                }
                Preconditions.checkNotNull(value);
                inlineWellDefinedVariable(v, value, referenceInfo.references);
                staleVars.add(v);
            } else if (refCount == firstRefAfterInit) {
                // The variable likely only read once, try some more
                // complex inlining heuristics.
                Reference reference = referenceInfo.references.get(firstRefAfterInit - 1);
                if (canInline(declaration, init, reference)) {
                    inline(v, declaration, init, reference);
                    staleVars.add(v);
                }
            } else if (declaration != init && refCount == 2) {
                if (isValidDeclaration(declaration) && isValidInitialization(init)) {
                    // The only reference is the initialization, remove the assignment and
                    // the variable declaration.
                    Node value = init.getAssignedValue();
                    Preconditions.checkNotNull(value);
                    inlineWellDefinedVariable(v, value, referenceInfo.references);
                    staleVars.add(v);
                }
            }

            // If this variable was not inlined normally, check if we can
            // inline an alias of it. (If the variable was inlined, then the
            // reference data is out of sync. We're better off just waiting for
            // the next pass.)
            if (!maybeModifiedArguments && !staleVars.contains(v) && referenceInfo.isWellDefined()
                    && referenceInfo.isAssignedOnceInLifetime()) {
                List<Reference> refs = referenceInfo.references;
                for (int i = 1 /* start from a read */; i < refs.size(); i++) {
                    Node nameNode = refs.get(i).getNode();
                    if (aliasCandidates.containsKey(nameNode)) {
                        AliasCandidate candidate = aliasCandidates.get(nameNode);
                        if (!staleVars.contains(candidate.alias) && !isVarInlineForbidden(candidate.alias)) {
                            Reference aliasInit;
                            aliasInit = candidate.refInfo.getInitializingReference();
                            Node value = aliasInit.getAssignedValue();
                            Preconditions.checkNotNull(value);
                            inlineWellDefinedVariable(candidate.alias, value, candidate.refInfo.references);
                            staleVars.add(candidate.alias);
                        }
                    }
                }
            }
        }

        /**
         * If there are any variable references in the given node tree, blacklist
         * them to prevent the pass from trying to inline the variable.
         */
        private void blacklistVarReferencesInTree(Node root, Scope scope) {
            for (Node c = root.getFirstChild(); c != null; c = c.getNext()) {
                blacklistVarReferencesInTree(c, scope);
            }

            if (root.isName()) {
                staleVars.add(scope.getVar(root.getString()));
            }
        }

        /**
         * Whether the given variable is forbidden from being inlined.
         */
        private boolean isVarInlineForbidden(Var var) {
            // A variable may not be inlined if:
            // 1) The variable is exported,
            // 2) A reference to the variable has been inlined. We're downstream
            //    of the mechanism that creates variable references, so we don't
            //    have a good way to update the reference. Just punt on it.
            // 3) Don't inline the special RENAME_PROPERTY_FUNCTION_NAME
            return var.isExtern() || compiler.getCodingConvention().isExported(var.name)
                    || RenameProperties.RENAME_PROPERTY_FUNCTION_NAME.equals(var.name) || staleVars.contains(var);
        }

        /**
         * Do the actual work of inlining a single declaration into a single
         * reference.
         */
        private void inline(Var v, Reference decl, Reference init, Reference ref) {
            Node value = init.getAssignedValue();
            Preconditions.checkState(value != null);
            // Check for function declarations before the value is moved in the AST.
            boolean isFunctionDeclaration = NodeUtil.isFunctionDeclaration(value);
            compiler.reportChangeToEnclosingScope(ref.getNode());
            inlineValue(v, ref, value.detachFromParent());
            if (decl != init) {
                Node expressRoot = init.getGrandparent();
                Preconditions.checkState(expressRoot.isExprResult());
                NodeUtil.removeChild(expressRoot.getParent(), expressRoot);
            }
            // Function declarations have already been removed.
            if (!isFunctionDeclaration) {
                compiler.reportChangeToEnclosingScope(decl.getNode());
                removeDeclaration(decl);
            }
        }

        /**
         * Inline an immutable variable into all of its references.
         */
        private void inlineWellDefinedVariable(Var v, Node value, List<Reference> refSet) {
            Reference decl = refSet.get(0);
            for (int i = 1; i < refSet.size(); i++) {
                inlineValue(v, refSet.get(i), value.cloneTree());
            }
            removeDeclaration(decl);
        }

        /**
         * Inline a declared constant.
         */
        private void inlineDeclaredConstant(Var v, Node value, List<Reference> refSet) {
            // Replace the references with the constant value
            Reference decl = null;

            for (Reference r : refSet) {
                if (r.getNode() == v.getNameNode()) {
                    decl = r;
                } else {
                    inlineValue(v, r, value.cloneTree());
                }
            }

            removeDeclaration(decl);
        }

        /**
         * Remove the given VAR declaration.
         */
        private void removeDeclaration(Reference decl) {
            Node varNode = decl.getParent();
            Node grandparent = decl.getGrandparent();

            compiler.reportChangeToEnclosingScope(decl.getNode());
            varNode.removeChild(decl.getNode());
            // Remove var node if empty
            if (!varNode.hasChildren()) {
                Preconditions.checkState(varNode.isVar());
                NodeUtil.removeChild(grandparent, varNode);
            }
        }

        /**
         * Replace the given reference with the given value node.
         *
         * @param v The variable that's referenced.
         * @param ref The reference to replace.
         * @param value The node tree to replace it with. This tree should be safe
         *     to re-parent.
         */
        private void inlineValue(Var v, Reference ref, Node value) {
            compiler.reportChangeToEnclosingScope(ref.getNode());
            if (ref.isSimpleAssignmentToName()) {
                // This is the initial assignment.
                ref.getGrandparent().replaceChild(ref.getParent(), value);
            } else {
                ref.getParent().replaceChild(ref.getNode(), value);
            }
            blacklistVarReferencesInTree(value, v.scope);
        }

        /**
         * Determines whether the given variable is declared as a constant
         * and may be inlined.
         */
        private boolean isInlineableDeclaredConstant(Var var, ReferenceCollection refInfo) {
            if (!identifyConstants.apply(var)) {
                return false;
            }

            if (!refInfo.isAssignedOnceInLifetime()) {
                return false;
            }

            Reference init = refInfo.getInitializingReferenceForConstants();
            if (init == null) {
                return false;
            }

            Node value = init.getAssignedValue();
            if (value == null) {
                // This constant is either externally defined or initialized indirectly
                // (e.g. in an function expression used to hide
                // temporary variables), so the constant is ineligible for inlining.
                return false;
            }

            // Is the constant's value immutable?
            if (!NodeUtil.isImmutableValue(value)) {
                return false;
            }

            // Determine if we should really inline a String or not.
            return !value.isString() || isStringWorthInlining(var, refInfo.references);
        }

        /**
         * Compute whether the given string is worth inlining.
         */
        private boolean isStringWorthInlining(Var var, List<Reference> refs) {
            if (!inlineAllStrings && !var.isDefine()) {
                int len = var.getInitialValue().getString().length() + "''".length();

                // if not inlined: var xx="value"; .. xx .. xx ..
                // The 4 bytes per reference is just a heuristic:
                // 2 bytes per var name plus maybe 2 bytes if we don't inline, e.g.
                // in the case of "foo " + CONST + " bar"
                int noInlineBytes = "var xx=;".length() + len + 4 * (refs.size() - 1);

                // if inlined:
                // I'm going to assume that half of the quotes will be eliminated
                // thanks to constant folding, therefore I subtract 1 (2/2=1) from
                // the string length.
                int inlineBytes = (len - 1) * (refs.size() - 1);

                // Not inlining if doing so uses more bytes, or this constant is being
                // defined.
                return noInlineBytes >= inlineBytes;
            }

            return true;
        }

        /**
         * @return true if the provided reference and declaration can be safely
         *         inlined according to our criteria
         */
        private boolean canInline(Reference declaration, Reference initialization, Reference reference) {
            if (!isValidDeclaration(declaration) || !isValidInitialization(initialization)
                    || !isValidReference(reference)) {
                return false;
            }

            // If the value is read more than once, skip it.
            // VAR declarations and EXPR_RESULT don't need the value, but other
            // ASSIGN expressions parents do.
            if (declaration != initialization && !initialization.getGrandparent().isExprResult()) {
                return false;
            }

            // Be very conservative and do no cross control structures or
            // scope boundaries
            if (declaration.getBasicBlock() != initialization.getBasicBlock()
                    || declaration.getBasicBlock() != reference.getBasicBlock()) {
                return false;
            }

            // Do not inline into a call node. This would change
            // the context in which it was being called. For example,
            //   var a = b.c;
            //   a();
            // should not be inlined, because it calls a in the context of b
            // rather than the context of the window.
            //   var a = b.c;
            //   f(a)
            // is OK.
            Node value = initialization.getAssignedValue();
            Preconditions.checkState(value != null);
            if (value.isGetProp() && reference.getParent().isCall()
                    && reference.getParent().getFirstChild() == reference.getNode()) {
                return false;
            }

            if (value.isFunction()) {
                Node callNode = reference.getParent();
                if (reference.getParent().isCall()) {
                    CodingConvention convention = compiler.getCodingConvention();
                    // Bug 2388531: Don't inline subclass definitions into class defining
                    // calls as this confused class removing logic.
                    SubclassRelationship relationship = convention.getClassesDefinedByCall(callNode);
                    if (relationship != null) {
                        return false;
                    }

                    // issue 668: Don't inline singleton getter methods
                    // calls as this confused class removing logic.
                    if (convention.getSingletonGetterClassName(callNode) != null) {
                        return false;
                    }
                }
            }

            return canMoveAggressively(value) || canMoveModerately(initialization, reference);
        }

        /**
         * If the value is a literal, we can cross more boundaries to inline it.
         */
        private boolean canMoveAggressively(Node value) {
            // Function expressions and other mutable objects can move within
            // the same basic block.
            return NodeUtil.isLiteralValue(value, true) || value.isFunction();
        }

        /**
         * If the value of a variable is not constant, then it may read or modify
         * state. Therefore it cannot be moved past anything else that may modify
         * the value being read or read values that are modified.
         */
        private boolean canMoveModerately(Reference initialization, Reference reference) {
            // Check if declaration can be inlined without passing
            // any side-effect causing nodes.
            Iterator<Node> it;
            if (initialization.getParent().isVar()) {
                it = NodeIterators.LocalVarMotion.forVar(initialization.getNode(), // NAME
                        initialization.getParent(), // VAR
                        initialization.getGrandparent()); // VAR container
            } else if (initialization.getParent().isAssign()) {
                Preconditions.checkState(initialization.getGrandparent().isExprResult());
                it = NodeIterators.LocalVarMotion.forAssign(initialization.getNode(), // NAME
                        initialization.getParent(), // ASSIGN
                        initialization.getGrandparent(), // EXPR_RESULT
                        initialization.getGrandparent().getParent()); // EXPR container
            } else {
                throw new IllegalStateException(
                        "Unexpected initialization parent " + initialization.getParent().toStringTree());
            }
            Node targetName = reference.getNode();
            while (it.hasNext()) {
                Node curNode = it.next();
                if (curNode == targetName) {
                    return true;
                }
            }

            return false;
        }

        /**
         * @return true if the reference is a normal VAR or FUNCTION declaration.
         */
        private boolean isValidDeclaration(Reference declaration) {
            return (declaration.getParent().isVar() && !declaration.getGrandparent().isFor())
                    || NodeUtil.isFunctionDeclaration(declaration.getParent());
        }

        /**
         * @return Whether there is a initial value.
         */
        private boolean isValidInitialization(Reference initialization) {
            if (initialization == null) {
                return false;
            } else if (initialization.isDeclaration()) {
                // The reference is a FUNCTION declaration or normal VAR declaration
                // with a value.
                if (!NodeUtil.isFunctionDeclaration(initialization.getParent())
                        && initialization.getNode().getFirstChild() == null) {
                    return false;
                }
            } else {
                Node parent = initialization.getParent();
                Preconditions.checkState(parent.isAssign() && parent.getFirstChild() == initialization.getNode());
            }

            Node n = initialization.getAssignedValue();
            if (n.isFunction()) {
                return compiler.getCodingConvention().isInlinableFunction(n);
            }

            return true;
        }

        /**
         * @return true if the reference is a candidate for inlining
         */
        private boolean isValidReference(Reference reference) {
            return !reference.isDeclaration() && !reference.isLvalue();
        }

        /**
         * Determines whether the reference collection describes a variable that
         * is initialized to an immutable value, never modified, and defined before
         * every reference.
         */
        private boolean isImmutableAndWellDefinedVariable(Var v, ReferenceCollection refInfo) {
            List<Reference> refSet = refInfo.references;
            int startingReadRef = 1;
            Reference refDecl = refSet.get(0);
            if (!isValidDeclaration(refDecl)) {
                return false;
            }

            boolean isNeverAssigned = refInfo.isNeverAssigned();
            // For values that are never assigned, only the references need to be
            // checked.
            if (!isNeverAssigned) {
                Reference refInit = refInfo.getInitializingReference();
                if (!isValidInitialization(refInit)) {
                    return false;
                }

                if (refDecl != refInit) {
                    Preconditions.checkState(refInit == refSet.get(1));
                    startingReadRef = 2;
                }

                if (!refInfo.isWellDefined()) {
                    return false;
                }

                Node value = refInit.getAssignedValue();
                Preconditions.checkNotNull(value);

                boolean isImmutableValueWorthInlining = NodeUtil.isImmutableValue(value)
                        && (!value.isString() || isStringWorthInlining(v, refInfo.references));
                boolean isInlinableThisAlias = value.isThis() && !refInfo.isEscaped();
                if (!isImmutableValueWorthInlining && !isInlinableThisAlias) {
                    return false;
                }
            }

            for (int i = startingReadRef; i < refSet.size(); i++) {
                Reference ref = refSet.get(i);
                if (!isValidReference(ref)) {
                    return false;
                }
            }

            return true;
        }
    }
}