-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
SyntacticScopeCreator.java
361 lines (313 loc) · 11.6 KB
/
SyntacticScopeCreator.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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
/*
* Copyright 2014 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.checkState;
import com.google.javascript.rhino.InputId;
import com.google.javascript.rhino.Node;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The syntactic scope creator scans the parse tree to create a Scope object containing all the
* variable declarations in that scope. This class adds support for block-level scopes introduced in
* ECMAScript 6.
*
* <p>This implementation is not thread-safe.
*
* @author moz@google.com (Michael Zhou)
*/
public class SyntacticScopeCreator implements ScopeCreator {
private final AbstractCompiler compiler;
private final RedeclarationHandler redeclarationHandler;
private final ScopeFactory scopeFactory;
// The arguments variable is special, in that it's declared for every function,
// but not explicitly declared.
private static final String ARGUMENTS = "arguments";
public static final RedeclarationHandler DEFAULT_REDECLARATION_HANDLER =
new DefaultRedeclarationHandler();
public SyntacticScopeCreator(AbstractCompiler compiler) {
this(compiler, DEFAULT_REDECLARATION_HANDLER);
}
public SyntacticScopeCreator(AbstractCompiler compiler, ScopeFactory scopeFactory) {
this(compiler, DEFAULT_REDECLARATION_HANDLER, scopeFactory);
}
SyntacticScopeCreator(AbstractCompiler compiler, RedeclarationHandler redeclarationHandler) {
this(compiler, redeclarationHandler, new DefaultScopeFactory());
}
SyntacticScopeCreator(
AbstractCompiler compiler,
RedeclarationHandler redeclarationHandler,
ScopeFactory scopeFactory) {
this.compiler = compiler;
this.redeclarationHandler = redeclarationHandler;
this.scopeFactory = scopeFactory;
}
@Override
public boolean hasBlockScope() {
return true;
}
/** A simple API for injecting the use of alternative Scope classes */
public interface ScopeFactory {
Scope create(Scope parent, Node n);
}
private static class DefaultScopeFactory implements ScopeFactory {
@Override
public Scope create(Scope parent, Node n) {
return (parent == null)
? Scope.createGlobalScope(n)
: Scope.createChildScope(parent, n);
}
}
@Override
public Scope createScope(Node n, AbstractScope<?, ?> parent) {
Scope scope = scopeFactory.create((Scope) parent, n);
new ScopeScanner(compiler, redeclarationHandler, scope, null).populate();
return scope;
}
/**
* A class to traverse the AST looking for name definitions and add them to the Scope.
*/
static class ScopeScanner {
private final Scope scope;
private final AbstractCompiler compiler;
private final RedeclarationHandler redeclarationHandler;
// Will be null, when a detached node is traversed.
@Nullable
private InputId inputId;
private final Set<Node> changeRootSet;
ScopeScanner(AbstractCompiler compiler, Scope scope) {
this(compiler, DEFAULT_REDECLARATION_HANDLER, scope, null);
}
ScopeScanner(
AbstractCompiler compiler, RedeclarationHandler redeclarationHandler, Scope scope,
Set<Node> changeRootSet) {
this.compiler = compiler;
this.redeclarationHandler = redeclarationHandler;
this.scope = scope;
this.changeRootSet = changeRootSet;
checkState(changeRootSet == null || scope.isGlobal());
}
void populate() {
Node n = scope.getRootNode();
// If we are populating the global scope, inputId will be null, and need to be set
// as we enter each SCRIPT node.
inputId = NodeUtil.getInputId(n);
switch (n.getToken()) {
case FUNCTION: {
final Node fnNameNode = n.getFirstChild();
final Node args = fnNameNode.getNext();
// Args: Declare function variables
checkState(args.isParamList());
declareLHS(scope, args);
// Bleed the function name into the scope, if it hasn't been declared in the outer scope
// and the name isn't already in the scope via the param list.
String fnName = fnNameNode.getString();
if (!fnName.isEmpty() && NodeUtil.isFunctionExpression(n)) {
declareVar(scope, fnNameNode);
}
// Since we create a separate scope for body, stop scanning here
return;
}
case CLASS: {
final Node classNameNode = n.getFirstChild();
// Bleed the class name into the scope, if it hasn't
// been declared in the outer scope.
if (!classNameNode.isEmpty() && NodeUtil.isClassExpression(n)) {
declareVar(scope, classNameNode);
}
return;
}
case ROOT:
case SCRIPT:
// n is the global scope
checkState(scope.isGlobal(), scope);
scanVars(n, scope, scope);
return;
case MODULE_BODY:
scanVars(n, scope, scope);
return;
case FOR:
case FOR_OF:
case FOR_AWAIT_OF:
case FOR_IN:
case SWITCH:
scanVars(n, null, scope);
return;
case BLOCK:
if (NodeUtil.isFunctionBlock(n)) {
scanVars(n, scope, scope);
} else {
scanVars(n, null, scope);
}
return;
default:
throw new RuntimeException("Illegal scope root: " + n);
}
}
private void declareLHS(Scope s, Node n) {
for (Node lhs : NodeUtil.findLhsNodesInNode(n)) {
declareVar(s, lhs);
}
}
/**
* Scans and gather variables declarations under a Node
*
* @param n The node
* @param hoistScope The scope that is the hoist target for vars, if we are scanning for vars.
* @param blockScope The scope that is the hoist target for block-level declarations, if we are
* scanning for block level declarations.
*/
private void scanVars(Node n, @Nullable Scope hoistScope, @Nullable Scope blockScope) {
switch (n.getToken()) {
case VAR:
if (hoistScope != null) {
declareLHS(hoistScope, n);
}
return;
case LET:
case CONST:
// Only declare when scope is the current lexical scope
if (blockScope != null) {
declareLHS(blockScope, n);
}
return;
case IMPORT:
declareLHS(hoistScope, n);
return;
case EXPORT:
// The first child of an EXPORT can be a declaration, in the case of
// export var/let/const/function/class name ...
scanVars(n.getFirstChild(), hoistScope, blockScope);
return;
case FUNCTION:
if (NodeUtil.isFunctionExpression(n) || blockScope == null) {
return;
}
String fnName = n.getFirstChild().getString();
if (fnName.isEmpty()) {
// This is invalid, but allow it so the checks can catch it.
return;
}
declareVar(blockScope, n.getFirstChild());
return; // should not examine function's children
case CLASS:
if (NodeUtil.isClassExpression(n) || blockScope == null) {
return;
}
String className = n.getFirstChild().getString();
if (className.isEmpty()) {
// This is invalid, but allow it so the checks can catch it.
return;
}
declareVar(blockScope, n.getFirstChild());
return; // should not examine class's children
case CATCH:
checkState(n.hasTwoChildren(), n);
// the first child is the catch var and the second child
// is the code block
if (blockScope != null) {
declareLHS(blockScope, n);
}
// A new scope is not created for this BLOCK because there is a scope
// created for the BLOCK above the CATCH
final Node block = n.getSecondChild();
scanVars(block, hoistScope, blockScope);
return; // only one child to scan
case SCRIPT:
if (changeRootSet != null && !changeRootSet.contains(n)) {
// If there is a changeRootSet configured, that means
// a partial update is being done and we should skip
// any SCRIPT that aren't being asked for.
return;
}
inputId = n.getInputId();
break;
case MODULE_BODY:
// Module bodies are not part of global scope.
if (hoistScope.isGlobal()) {
return;
}
break;
default:
break;
}
boolean isBlockStart = blockScope != null && n == blockScope.getRootNode();
boolean enteringNewBlock = !isBlockStart && NodeUtil.createsBlockScope(n);
if (enteringNewBlock && hoistScope == null) {
// We only enter new blocks when scanning for hoisted vars
return;
}
// Variables can only occur in statement-level nodes, so
// we only need to traverse children in a couple special cases.
if (NodeUtil.isControlStructure(n) || NodeUtil.isStatementBlock(n)) {
for (Node child = n.getFirstChild(); child != null;) {
Node next = child.getNext();
scanVars(child, hoistScope, enteringNewBlock ? null : blockScope);
child = next;
}
}
}
/**
* Declares a variable.
*
* @param s The scope to declare the variable in.
* @param n The node corresponding to the variable name.
*/
private void declareVar(Scope s, Node n) {
checkState(n.isName() || n.isImportStar(), "Invalid node for declareVar: %s", n);
String name = n.getString();
// Because of how we scan the variables, it is possible to encounter
// the same var declared name node twice. Bail out in this case.
// TODO(johnlenz): Hash lookups are not free and building scopes are already expensive.
// Restructure the scope building to avoid this check.
Var v = s.getOwnSlot(name);
if (v != null && v.getNode() == n) {
return;
}
CompilerInput input = compiler.getInput(inputId);
if (v != null
|| !isShadowingAllowed(name, s)
|| ((s.isFunctionScope()
|| s.isFunctionBlockScope()) && name.equals(ARGUMENTS))) {
redeclarationHandler.onRedeclaration(s, name, n, input);
} else {
s.declare(name, n, input);
}
}
// Function body declarations are not allowed to shadow
// function parameters.
private static boolean isShadowingAllowed(String name, Scope s) {
if (s.isFunctionBlockScope()) {
Var maybeParam = s.getParent().getOwnSlot(name);
return maybeParam == null || !maybeParam.isParam();
}
return true;
}
}
/**
* Interface for injectable duplicate handling.
*/
interface RedeclarationHandler {
void onRedeclaration(
Scope s, String name, Node n, CompilerInput input);
}
/**
* The default handler for duplicate declarations.
*/
static class DefaultRedeclarationHandler implements RedeclarationHandler {
@Override
public void onRedeclaration(Scope s, String name, Node n, CompilerInput input) {}
}
}