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

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.NameBasedDefinitionProvider.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.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.DefinitionsRemover.Definition;
import com.google.javascript.jscomp.DefinitionsRemover.ExternalNameOnlyDefinition;
import com.google.javascript.jscomp.DefinitionsRemover.UnknownDefinition;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.NodeTraversal.ChangeScopeRootCallback;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

/**
 * Simple name-based definition gatherer.
 *
 * <p>It treats all variable writes as happening in the global scope and treats all objects as
 * capable of having the same set of properties. The current implementation only handles definitions
 * whose right hand side is an immutable value or function expression. All complex definitions are
 * treated as unknowns.
 *
 * <p>This definition simply uses the variable name to determine a new definition site so
 * potentially it could return multiple definition sites for a single variable. Although we could
 * use the type system to make this more accurate, in practice after disambiguate properties has
 * run, names are unique enough that this works well enough to accept the performance gain.
 */
public class NameBasedDefinitionProvider implements CompilerPass {

    protected final AbstractCompiler compiler;

    protected final Multimap<String, Definition> definitionsByName;
    protected final Map<Node, DefinitionSite> definitionSitesByDefinitionSiteNode;
    protected final Multimap<Node, DefinitionSite> definitionSitesByScopeNode;
    protected final Set<Node> definitionNodes;

    protected final boolean allowComplexFunctionDefs;
    protected boolean hasProcessBeenRun = false;

    public NameBasedDefinitionProvider(AbstractCompiler compiler, boolean allowComplexFunctionDefs) {
        this.compiler = compiler;
        this.allowComplexFunctionDefs = allowComplexFunctionDefs;
        int numInputs = compiler.getNumberOfInputs();
        // Estimates below were generated by experimentation with large Google projects.
        this.definitionsByName = LinkedHashMultimap.create(numInputs * 15, 1);
        int estimatedDefinitionSites = numInputs * 22;
        this.definitionSitesByDefinitionSiteNode = Maps.newLinkedHashMapWithExpectedSize(estimatedDefinitionSites);
        this.definitionSitesByScopeNode = HashMultimap.create(estimatedDefinitionSites, 1);
        this.definitionNodes = Sets.newHashSetWithExpectedSize(estimatedDefinitionSites);
    }

    @Override
    public void process(Node externs, Node source) {
        checkState(!hasProcessBeenRun, "The definition provider is already initialized.");

        this.hasProcessBeenRun = true;

        NodeTraversal.traverse(compiler, externs, new DefinitionGatheringCallback(true));
        dropUntypedExterns();

        NodeTraversal.traverse(compiler, source, new DefinitionGatheringCallback(false));
    }

    /** @return Whether the node has a JSDoc that actually declares something. */
    private boolean jsdocContainsDeclarations(Node node) {
        JSDocInfo info = node.getJSDocInfo();
        return (info != null && info.containsDeclaration());
    }

    /**
     * Drop untyped stub definitions (ExternalNameOnlyDefinition) in externs if a typed extern of the
     * same qualified name also exists and has type annotations.
     *
     * <p>TODO: This hack is mostly for the purpose of preventing untyped stubs from showing up in the
     * {@link PureFunctionIdentifier} and causing unknown side effects from propagating everywhere.
     * This should probably be solved in one of the following ways instead:
     *
     * <p>a) Have a pass earlier in the compiler that goes in and removes these stub definitions.
     *
     * <p>b) Fix all extern files so that there are no untyped stubs mixed with typed ones and add a
     * restriction to the compiler to prevent this.
     *
     * <p>c) Drop these stubs in the {@link PureFunctionIdentifier} instead. This "DefinitionProvider"
     * should not have to drop definitions itself.
     */
    private void dropUntypedExterns() {
        for (String name : definitionsByName.keySet()) {
            for (Definition definition : new ArrayList<>(definitionsByName.get(name))) {
                if (!(definition instanceof ExternalNameOnlyDefinition)) {
                    continue;
                }
                Node definitionNode = definition.getLValue();
                if (jsdocContainsDeclarations(definitionNode)) {
                    continue;
                }

                for (Definition previousDefinition : definitionsByName.get(name)) {
                    if (previousDefinition != definition
                            && definitionNode.matchesQualifiedName(previousDefinition.getLValue())) {
                        // *DON'T* remove from definitionNodes since it is desired to retain references to
                        // stub definitions.
                        definitionsByName.remove(name, definition);
                        DefinitionSite definitionSite = definitionSitesByDefinitionSiteNode.remove(definitionNode);
                        Node scopeNode = NodeUtil.getEnclosingChangeScopeRoot(definitionNode);
                        definitionSitesByScopeNode.remove(scopeNode, definitionSite);

                        // Since it's a stub we know its keyed by the name/getProp node.
                        checkNotNull(definitionSite);
                        break;
                    }
                }
            }
        }
    }

    /**
     * Returns a collection of definitions that characterize the possible values of a variable or
     * property.
     */
    public Collection<Definition> getDefinitionsReferencedAt(Node useSiteNode) {
        checkState(hasProcessBeenRun, "Hasn't been initialized with process() yet.");
        checkArgument(useSiteNode.isGetProp() || useSiteNode.isName(), useSiteNode);

        if (definitionNodes.contains(useSiteNode)) {
            return ImmutableList.of();
        }

        if (useSiteNode.isGetProp()) {
            String propName = useSiteNode.getLastChild().getString();
            if (propName.equals("apply") || propName.equals("call")) {
                useSiteNode = useSiteNode.getFirstChild();
            }
        }

        String name = DefinitionsRemover.Definition.getSimplifiedName(useSiteNode);
        if (name != null) {
            return definitionsByName.get(name);
        }
        return ImmutableList.of();
    }

    private class DefinitionGatheringCallback implements Callback, ChangeScopeRootCallback {

        DefinitionGatheringCallback() {
        }

        DefinitionGatheringCallback(boolean inExterns) {
            this.inExterns = inExterns;
        }

        @Override
        public void enterChangeScopeRoot(AbstractCompiler compiler, Node root) {
            this.inExterns = root.isFromExterns();
        }

        private boolean inExterns;

        @Override
        public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
            if (inExterns) {
                if (n.isFunction() && !n.getFirstChild().isName()) {
                    // No need to crawl functions in JSDoc
                    return false;
                }
                if (parent != null && parent.isFunction() && n != parent.getFirstChild()) {
                    // Arguments of external functions should not count as name
                    // definitions.  They are placeholder names for documentation
                    // purposes only which are not reachable from anywhere.
                    return false;
                }
            }
            return true;
        }

        @Override
        public void visit(NodeTraversal traversal, Node node, Node parent) {
            if (inExterns) {
                visitExterns(traversal, node);
            } else {
                visitCode(traversal, node);
            }
        }

        private void visitExterns(NodeTraversal traversal, Node node) {
            if (node.getJSDocInfo() != null) {
                for (Node typeRoot : node.getJSDocInfo().getTypeNodes()) {
                    traversal.traverse(typeRoot);
                }
            }

            Definition definition = DefinitionsRemover.getDefinition(node, true);
            if (definition != null) {
                String name = definition.getSimplifiedName();
                if (name != null) {
                    Node rValue = definition.getRValue();
                    if ((rValue != null) && !NodeUtil.isImmutableValue(rValue) && !rValue.isFunction()) {
                        // Unhandled complex expression
                        Definition unknownDefinition = new UnknownDefinition(definition.getLValue(), true);
                        definition = unknownDefinition;
                    }
                    addDefinition(name, definition, node, traversal);
                }
            }
        }

        private void visitCode(NodeTraversal traversal, Node node) {
            Definition definition = DefinitionsRemover.getDefinition(node, false);

            if (definition != null) {
                String name = definition.getSimplifiedName();
                if (name != null) {
                    Node rValue = definition.getRValue();
                    if (rValue != null && !NodeUtil.isImmutableValue(rValue)
                            && !isKnownFunctionDefinition(rValue)) {
                        // Unhandled complex expression
                        definition = new UnknownDefinition(definition.getLValue(), false);
                    }
                    addDefinition(name, definition, node, traversal);
                }
            }
        }

        boolean isKnownFunctionDefinition(Node n) {
            switch (n.getToken()) {
            case FUNCTION:
                return true;
            case HOOK:
                return allowComplexFunctionDefs && isKnownFunctionDefinition(n.getSecondChild())
                        && isKnownFunctionDefinition(n.getLastChild());
            default:
                return false;
            }
        }
    }

    private void addDefinition(String name, Definition definition, Node definitionSiteNode,
            NodeTraversal traversal) {
        Node definitionNode = definition.getLValue();

        definitionNodes.add(definitionNode);
        definitionsByName.put(name, definition);
        DefinitionSite definitionSite = new DefinitionSite(definitionSiteNode, definition, traversal.getModule(),
                traversal.inGlobalScope(), definition.isExtern());
        definitionSitesByDefinitionSiteNode.put(definitionSiteNode, definitionSite);
        Node scopeNode = NodeUtil.getEnclosingChangeScopeRoot(definitionSiteNode);
        definitionSitesByScopeNode.put(scopeNode, definitionSite);
    }

    /**
     * Returns the collection of definition sites found during traversal.
     *
     * @return definition site collection.
     */
    public Collection<DefinitionSite> getDefinitionSites() {
        checkState(hasProcessBeenRun, "Hasn't been initialized with process() yet.");
        return definitionSitesByDefinitionSiteNode.values();
    }
}