com.google.javascript.jscomp.NameBasedDefinitionProvider Maven / Gradle / Ivy
Show all versions of closure-compiler Show documentation
/*
* 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 com.google.common.base.Preconditions;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
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.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Simple name-based definition gatherer that implements {@link DefinitionProvider}.
*
* 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 DefinitionProvider, CompilerPass {
protected final Multimap nameDefinitionMultimap = LinkedHashMultimap.create();
protected final Map definitionNodeByDefinitionSite = new LinkedHashMap<>();
protected final AbstractCompiler compiler;
protected final boolean allowComplexFunctionDefs;
protected boolean hasProcessBeenRun = false;
public NameBasedDefinitionProvider(AbstractCompiler compiler, boolean allowComplexFunctionDefs) {
this.compiler = compiler;
this.allowComplexFunctionDefs = allowComplexFunctionDefs;
}
@Override
public void process(Node externs, Node source) {
Preconditions.checkState(!hasProcessBeenRun, "The definition provider is already initialized.");
this.hasProcessBeenRun = true;
NodeTraversal.traverseEs6(compiler, externs, new DefinitionGatheringCallback(true));
NodeTraversal.traverseEs6(compiler, source, new DefinitionGatheringCallback(false));
}
@Override
public Collection getDefinitionsReferencedAt(Node useSite) {
Preconditions.checkState(hasProcessBeenRun, "The process was not run");
Preconditions.checkArgument(useSite.isGetProp() || useSite.isName());
if (definitionNodeByDefinitionSite.containsKey(useSite)) {
return null;
}
if (useSite.isGetProp()) {
String propName = useSite.getLastChild().getString();
if (propName.equals("apply") || propName.equals("call")) {
useSite = useSite.getFirstChild();
}
}
String name = getSimplifiedName(useSite);
if (name != null) {
Collection defs = nameDefinitionMultimap.get(name);
return defs.isEmpty() ? null : defs;
}
return null;
}
private class DefinitionGatheringCallback implements Callback {
private final boolean inExterns;
DefinitionGatheringCallback(boolean inExterns) {
this.inExterns = 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, parent);
} else {
visitCode(traversal, node);
}
}
private void visitExterns(NodeTraversal traversal, Node node, Node parent) {
if (node.getJSDocInfo() != null) {
for (Node typeRoot : node.getJSDocInfo().getTypeNodes()) {
traversal.traverse(typeRoot);
}
}
Definition def = DefinitionsRemover.getDefinition(node, true);
if (def != null) {
String name = getSimplifiedName(def.getLValue());
if (name != null) {
Node rValue = def.getRValue();
if ((rValue != null) && !NodeUtil.isImmutableValue(rValue) && !rValue.isFunction()) {
// Unhandled complex expression
Definition unknownDef = new UnknownDefinition(def.getLValue(), true);
def = unknownDef;
}
// TODO(johnlenz) : remove this stub dropping code if it becomes
// illegal to have untyped stubs in the externs definitions.
// We need special handling of untyped externs stubs here:
// the stub should be dropped if the name is provided elsewhere.
// If there is no qualified name for this, then there will be
// no stubs to remove. This will happen if node is an object
// literal key.
if (node.isQualifiedName()) {
for (Definition prevDef : new ArrayList<>(nameDefinitionMultimap.get(name))) {
if (prevDef instanceof ExternalNameOnlyDefinition
&& !jsdocContainsDeclarations(node)) {
if (node.matchesQualifiedName(prevDef.getLValue())) {
// Drop this stub, there is a real definition.
nameDefinitionMultimap.remove(name, prevDef);
}
}
}
}
addDefinition(name, def, node, traversal);
}
}
if (parent != null && parent.isExprResult()) {
String name = getSimplifiedName(node);
if (name != null) {
// TODO(johnlenz) : remove this code if it becomes illegal to have
// stubs in the externs definitions.
// We need special handling of untyped externs stubs here:
// the stub should be dropped if the name is provided elsewhere.
// We can't just drop the stub now as it needs to be used as the
// externs definition if no other definition is provided.
boolean dropStub = false;
if (!jsdocContainsDeclarations(node) && node.isQualifiedName()) {
for (Definition prevDef : nameDefinitionMultimap.get(name)) {
if (node.matchesQualifiedName(prevDef.getLValue())) {
dropStub = true;
break;
}
}
}
if (!dropStub) {
// Incomplete definition
Definition definition = new ExternalNameOnlyDefinition(node);
addDefinition(name, definition, node, traversal);
}
}
}
}
private void visitCode(NodeTraversal traversal, Node node) {
Definition def = DefinitionsRemover.getDefinition(node, false);
if (def != null) {
String name = getSimplifiedName(def.getLValue());
if (name != null) {
Node rValue = def.getRValue();
if (rValue != null
&& !NodeUtil.isImmutableValue(rValue)
&& !isKnownFunctionDefinition(rValue)) {
// Unhandled complex expression
def = new UnknownDefinition(def.getLValue(), false);
}
addDefinition(name, def, 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;
}
}
/** @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());
}
}
private void addDefinition(String name, Definition def, Node node, NodeTraversal traversal) {
nameDefinitionMultimap.put(name, def);
definitionNodeByDefinitionSite.put(
node,
new DefinitionSite(
node, def, traversal.getModule(), traversal.inGlobalScope(), def.isExtern()));
}
/**
* 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}.
*/
protected 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();
}
return null;
}
/**
* Returns the collection of definition sites found during traversal.
*
* @return definition site collection.
*/
public Collection getDefinitionSites() {
Preconditions.checkState(hasProcessBeenRun, "The process was not run");
return definitionNodeByDefinitionSite.values();
}
public DefinitionSite getDefinitionForFunction(Node function) {
Preconditions.checkState(hasProcessBeenRun, "The process was not run");
Preconditions.checkState(function.isFunction());
return definitionNodeByDefinitionSite.get(NodeUtil.getNameNode(function));
}
}