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

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

There is a newer version: 9.0.8
Show newest version
/*
 * 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.Iterables;
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.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Simple name-based definition gatherer.
 *
 * 

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. * *

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 definitionsByName; protected final Map definitionSitesByDefinitionSiteNode; protected final Multimap definitionSitesByScopeNode; protected final Set 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.traverseEs6(compiler, externs, new DefinitionGatheringCallback(true)); dropUntypedExterns(); NodeTraversal.traverseEs6(compiler, source, new DefinitionGatheringCallback(false)); } public void rebuildScopeRoots(List changedScopeRoots, List deletedScopeRoots) { for (Node scopeRoot : Iterables.concat(deletedScopeRoots, changedScopeRoots)) { for (DefinitionSite definitionSite : definitionSitesByScopeNode.removeAll(scopeRoot)) { Definition definition = definitionSite.definition; definitionNodes.remove(definitionSite.node); definitionsByName.remove(definition.getSimplifiedName(), definition); definitionSitesByDefinitionSiteNode.remove(definitionSite.node); } } DefinitionGatheringCallback cb = new DefinitionGatheringCallback(); NodeTraversal.traverseEs6ScopeRoots(compiler, null, changedScopeRoots, cb, cb, 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. * *

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: * *

a) Have a pass earlier in the compiler that goes in and removes these stub definitions. * *

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. * *

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 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 = 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); } /** * Extract a name from a node. In the case of GETPROP nodes, replace the namespace or object * expression with "this" for simplicity and correctness at the expense of inefficiencies due to * higher chances of name collisions. * *

TODO(user) revisit. it would be helpful to at least use fully qualified names in the case of * namespaces. Might not matter as much if this pass runs after {@link CollapseProperties}. */ @Nullable public static String getSimplifiedName(Node node) { if (node.isName()) { String name = node.getString(); if (name != null && !name.isEmpty()) { return name; } else { return null; } } else if (node.isGetProp()) { return "this." + node.getLastChild().getString(); } else if (node.isMemberFunctionDef()) { return "this." + node.getString(); } return null; } /** * Returns the collection of definition sites found during traversal. * * @return definition site collection. */ public Collection getDefinitionSites() { checkState(hasProcessBeenRun, "Hasn't been initialized with process() yet."); return definitionSitesByDefinitionSiteNode.values(); } public DefinitionSite getDefinitionForFunction(Node function) { checkState(hasProcessBeenRun, "Hasn't been initialized with process() yet."); checkState(function.isFunction()); return definitionSitesByDefinitionSiteNode.get(NodeUtil.getNameNode(function)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy