-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
NameBasedDefinitionProvider.java
243 lines (214 loc) · 9.35 KB
/
NameBasedDefinitionProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
* 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}.
*
* <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 DefinitionProvider, CompilerPass {
protected final Multimap<String, Definition> nameDefinitionMultimap = LinkedHashMultimap.create();
protected final Map<Node, DefinitionSite> definitionNodeByDefinitionSite = new LinkedHashMap<>();
protected final AbstractCompiler compiler;
protected boolean hasProcessBeenRun = false;
public NameBasedDefinitionProvider(AbstractCompiler compiler) {
this.compiler = compiler;
}
@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<Definition> 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<Definition> 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 && node.getJSDocInfo() != null) {
for (Node typeRoot : node.getJSDocInfo().getTypeNodes()) {
traversal.traverse(typeRoot);
}
}
Definition def = DefinitionsRemover.getDefinition(node, inExterns);
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(), inExterns);
def = unknownDef;
}
// TODO(johnlenz) : remove this stub dropping code if it becomes
// illegal to have untyped stubs in the externs definitions.
if (inExterns) {
// 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);
}
}
}
}
}
nameDefinitionMultimap.put(name, def);
definitionNodeByDefinitionSite.put(
node,
new DefinitionSite(
node, def, traversal.getModule(), traversal.inGlobalScope(), inExterns));
}
}
if (inExterns && (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);
nameDefinitionMultimap.put(name, definition);
definitionNodeByDefinitionSite.put(
node,
new DefinitionSite(
node, definition, traversal.getModule(), traversal.inGlobalScope(), inExterns));
}
}
}
}
/** @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());
}
}
/**
* 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.
*
* <p>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<DefinitionSite> 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));
}
}