/
VarCheck.java
453 lines (401 loc) · 16.6 KB
/
VarCheck.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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
/*
* Copyright 2004 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.jscomp.Es6SyntacticScopeCreator.RedeclarationHandler;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* Checks that all variables are declared, that file-private variables are
* accessed only in the file that declares them, and that any var references
* that cross module boundaries respect declared module dependencies.
*
*/
class VarCheck extends AbstractPostOrderCallback implements
HotSwapCompilerPass {
static final DiagnosticType UNDEFINED_VAR_ERROR = DiagnosticType.error(
"JSC_UNDEFINED_VARIABLE",
"variable {0} is undeclared");
static final DiagnosticType VIOLATED_MODULE_DEP_ERROR =
DiagnosticType.error(
"JSC_VIOLATED_MODULE_DEPENDENCY",
"module {0} cannot reference {2}, defined in module {1}, since {1} loads after {0}");
static final DiagnosticType MISSING_MODULE_DEP_ERROR =
DiagnosticType.warning(
"JSC_MISSING_MODULE_DEPENDENCY",
"missing module dependency; module {0} should depend"
+ " on module {1} because it references {2}");
static final DiagnosticType STRICT_MODULE_DEP_ERROR = DiagnosticType.disabled(
"JSC_STRICT_MODULE_DEPENDENCY",
// The newline below causes the JS compiler not to complain when the
// referenced module's name changes because, for example, it's a
// synthetic module.
"cannot reference {2} because of a missing module dependency\n"
+ "defined in module {1}, referenced from module {0}");
static final DiagnosticType NAME_REFERENCE_IN_EXTERNS_ERROR =
DiagnosticType.warning(
"JSC_NAME_REFERENCE_IN_EXTERNS",
"accessing name {0} in externs has no effect."
+ " Perhaps you forgot to add a var keyword?");
static final DiagnosticType UNDEFINED_EXTERN_VAR_ERROR =
DiagnosticType.warning(
"JSC_UNDEFINED_EXTERN_VAR_ERROR",
"name {0} is not defined in the externs.");
static final DiagnosticType VAR_MULTIPLY_DECLARED_ERROR =
DiagnosticType.error(
"JSC_VAR_MULTIPLY_DECLARED_ERROR",
"Variable {0} declared more than once. First occurrence: {1}");
static final DiagnosticType VAR_ARGUMENTS_SHADOWED_ERROR =
DiagnosticType.error(
"JSC_VAR_ARGUMENTS_SHADOWED_ERROR",
"Shadowing \"arguments\" is not allowed");
static final DiagnosticType LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR =
DiagnosticType.error(
"JSC_LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR",
"Duplicate let / const / class declaration in the same scope is not allowed.");
// The arguments variable is special, in that it's declared in every local
// scope, but not explicitly declared.
private static final String ARGUMENTS = "arguments";
// Vars that still need to be declared in externs. These will be declared
// at the end of the pass, or when we see the equivalent var declared
// in the normal code.
private final Set<String> varsToDeclareInExterns = new HashSet<>();
private final AbstractCompiler compiler;
// Whether this is the post-processing validity check.
private final boolean validityCheck;
// Whether extern checks emit error.
private final boolean strictExternCheck;
private RedeclarationCheckHandler dupHandler;
VarCheck(AbstractCompiler compiler) {
this(compiler, false);
}
VarCheck(AbstractCompiler compiler, boolean validityCheck) {
this.compiler = compiler;
this.strictExternCheck = compiler.getErrorLevel(
JSError.make("", 0, 0, UNDEFINED_EXTERN_VAR_ERROR)) == CheckLevel.ERROR;
this.validityCheck = validityCheck;
}
/**
* Creates the scope creator used by this pass. If not in validity check mode, use a {@link
* RedeclarationCheckHandler} to check var redeclarations.
*/
private ScopeCreator createScopeCreator() {
if (validityCheck) {
return new Es6SyntacticScopeCreator(compiler);
} else {
dupHandler = new RedeclarationCheckHandler();
return new Es6SyntacticScopeCreator(compiler, dupHandler);
}
}
@Override
public void process(Node externs, Node root) {
ScopeCreator scopeCreator = createScopeCreator();
// Don't run externs-checking in sanity check mode. Normalization will
// remove duplicate VAR declarations, which will make
// externs look like they have assigns.
if (!validityCheck) {
NodeTraversal traversal = new NodeTraversal(
compiler, new NameRefInExternsCheck(), scopeCreator);
traversal.traverse(externs);
}
NodeTraversal t = new NodeTraversal(compiler, this, scopeCreator);
t.traverseRoots(externs, root);
for (String varName : varsToDeclareInExterns) {
createSynthesizedExternVar(varName);
}
if (dupHandler != null) {
dupHandler.removeDuplicates();
}
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
checkState(scriptRoot.isScript());
ScopeCreator scopeCreator = createScopeCreator();
NodeTraversal t = new NodeTraversal(compiler, this, scopeCreator);
// Note we use the global scope to prevent wrong "undefined-var errors" on
// variables that are defined in other JS files.
Scope topScope = scopeCreator.createScope(compiler.getRoot(), null);
t.traverseWithScope(scriptRoot, topScope);
// TODO(bashir) Check if we need to createSynthesizedExternVar like process.
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName() || (n.isStringKey() && !n.hasChildren())) {
String varName = n.getString();
// Only a function can have an empty name.
if (varName.isEmpty()) {
checkState(parent.isFunction());
checkState(NodeUtil.isFunctionExpression(parent));
return;
}
// Check if this is a declaration for a var that has been declared
// elsewhere. If so, mark it as a duplicate.
if ((parent.isVar()
|| NodeUtil.isFunctionDeclaration(parent))
&& varsToDeclareInExterns.contains(varName)) {
createSynthesizedExternVar(varName);
JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
builder.addSuppression("duplicate");
n.setJSDocInfo(builder.build());
}
// Check that the var has been declared.
Scope scope = t.getScope();
Var var = scope.getVar(varName);
if (var == null) {
if (NodeUtil.isFunctionExpression(parent)
|| (NodeUtil.isClassExpression(parent) && n == parent.getFirstChild())) {
// e.g. [ function foo() {} ], it's okay if "foo" isn't defined in the
// current scope.
} else {
boolean isArguments = scope.isFunctionScope() && ARGUMENTS.equals(varName);
// The extern checks are stricter, don't report a second error.
if (!isArguments && !(strictExternCheck && t.getInput().isExtern())) {
t.report(n, UNDEFINED_VAR_ERROR, varName);
}
if (validityCheck) {
// When the code is initially traversed, any undeclared variables are treated as
// externs. During this sanity check, we ensure that all variables have either been
// declared or marked as an extern. A failure at this point means that we have created
// some variable/generated some code with an undefined reference.
throw new IllegalStateException("Unexpected variable " + varName);
} else {
createSynthesizedExternVar(varName);
scope.getGlobalScope().declare(varName, n, compiler.getSynthesizedExternsInput());
}
}
return;
}
CompilerInput currInput = t.getInput();
CompilerInput varInput = var.input;
if (currInput == varInput || currInput == null || varInput == null) {
// The variable was defined in the same file. This is fine.
return;
}
// Check module dependencies.
JSModule currModule = currInput.getModule();
JSModule varModule = varInput.getModule();
JSModuleGraph moduleGraph = compiler.getModuleGraph();
if (!validityCheck && varModule != currModule && varModule != null && currModule != null) {
if (moduleGraph.dependsOn(currModule, varModule)) {
// The module dependency was properly declared.
} else {
if (scope.isGlobal()) {
if (moduleGraph.dependsOn(varModule, currModule)) {
// The variable reference violates a declared module dependency.
t.report(n, VIOLATED_MODULE_DEP_ERROR,
currModule.getName(), varModule.getName(), varName);
} else {
// The variable reference is between two modules that have no
// dependency relationship. This should probably be considered an
// error, but just issue a warning for now.
t.report(n, MISSING_MODULE_DEP_ERROR,
currModule.getName(), varModule.getName(), varName);
}
} else {
t.report(n, STRICT_MODULE_DEP_ERROR,
currModule.getName(), varModule.getName(), varName);
}
}
}
}
}
/**
* Create a new variable in a synthetic script. This will prevent
* subsequent compiler passes from crashing.
*/
static void createSynthesizedExternVar(AbstractCompiler compiler, String varName) {
Node nameNode = IR.name(varName);
// Mark the variable as constant if it matches the coding convention
// for constant vars.
// NOTE(nicksantos): honestly, I'm not sure how much this matters.
// AFAIK, all people who use the CONST coding convention also
// compile with undeclaredVars as errors. We have some test
// cases for this configuration though, and it makes them happier.
if (compiler.getCodingConvention().isConstant(varName)) {
nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true);
}
Node syntheticExternVar = IR.var(nameNode);
getSynthesizedExternsRoot(compiler).addChildToBack(syntheticExternVar);
compiler.reportChangeToEnclosingScope(syntheticExternVar);
}
/**
* Create a new variable in a synthetic script. This will prevent
* subsequent compiler passes from crashing.
*/
private void createSynthesizedExternVar(String varName) {
createSynthesizedExternVar(compiler, varName);
varsToDeclareInExterns.remove(varName);
}
/**
* A check for name references in the externs inputs. These used to prevent
* a variable from getting renamed, but no longer have any effect.
*/
private class NameRefInExternsCheck extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName()) {
switch (parent.getToken()) {
case VAR:
case LET:
case CONST:
case FUNCTION:
case CLASS:
case PARAM_LIST:
case DEFAULT_VALUE:
case REST:
case ARRAY_PATTERN:
// These are okay.
return;
case STRING_KEY:
if (parent.getParent().isObjectPattern()) {
return;
}
break;
case GETPROP:
if (n == parent.getFirstChild()) {
Scope scope = t.getScope();
Var var = scope.getVar(n.getString());
if (var == null) {
t.report(n, UNDEFINED_EXTERN_VAR_ERROR, n.getString());
varsToDeclareInExterns.add(n.getString());
}
}
return;
case ASSIGN:
// Don't warn for the "window.foo = foo;" nodes added by
// DeclaredGlobalExternsOnWindow, nor for alias declarations
// of the form "/** @const */ ns.Foo = Bar;"
if (n == parent.getLastChild() && n.isQualifiedName()
&& parent.getFirstChild().isQualifiedName()) {
return;
}
break;
case NAME:
// Don't warn for simple var assignments "/** @const */ var foo = bar;"
// They are used to infer the types of namespace aliases.
if (NodeUtil.isNameDeclaration(parent.getParent())) {
return;
}
break;
case OR:
// Don't warn for namespace declarations: "/** @const */ var ns = ns || {};"
if (NodeUtil.isNamespaceDecl(parent.getParent())) {
return;
}
break;
default:
break;
}
t.report(n, NAME_REFERENCE_IN_EXTERNS_ERROR, n.getString());
Scope scope = t.getScope();
Var var = scope.getVar(n.getString());
if (var == null) {
varsToDeclareInExterns.add(n.getString());
}
}
}
}
/**
* @param n The name node to check.
* @param origVar The associated Var.
* @return Whether duplicated declarations warnings should be suppressed
* for the given node.
*/
static boolean hasDuplicateDeclarationSuppression(Node n, Var origVar) {
checkState(n.isName() || n.isRest() || n.isStringKey() || n.isImportStar(), n);
Node parent = n.getParent();
Node origParent = origVar.getParentNode();
if (isExternNamespace(n)) {
return true;
}
JSDocInfo info = parent.getJSDocInfo();
if (info != null && info.getSuppressions().contains("duplicate")) {
return true;
}
info = origParent.getJSDocInfo();
return (info != null && info.getSuppressions().contains("duplicate"));
}
private static boolean isExternNamespace(Node n) {
return n.getParent().isVar() && n.isFromExterns() && NodeUtil.isNamespaceDecl(n);
}
/**
* The handler for duplicate declarations.
*/
private class RedeclarationCheckHandler implements RedeclarationHandler {
private ArrayList<Node> dupDeclNodes = new ArrayList<>();
@Override
public void onRedeclaration(
Scope s, String name, Node n, CompilerInput input) {
Node parent = n.getParent();
Var origVar = s.getVar(name);
Node origParent = origVar.getParentNode();
if (parent.isLet()
|| parent.isConst()
|| parent.isClass()
|| (origParent != null
&& (origParent.isLet() || origParent.isConst() || origParent.isClass()))) {
compiler.report(JSError.make(n, LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR));
return;
}
// Don't allow multiple variables to be declared at the top-level scope
if (s.isGlobal()) {
if (origParent.isCatch() && parent.isCatch()) {
// Okay, both are 'catch(x)' variables.
return;
}
boolean allowDupe = hasDuplicateDeclarationSuppression(n, origVar);
if (isExternNamespace(n)) {
this.dupDeclNodes.add(parent);
return;
}
if (!allowDupe) {
compiler.report(
JSError.make(n,
VAR_MULTIPLY_DECLARED_ERROR,
name,
(origVar.input != null
? origVar.input.getName()
: "??")));
}
} else if (name.equals(ARGUMENTS)
&& !(NodeUtil.isNameDeclaration(n.getParent()) && n.isName())) {
// Disallow shadowing "arguments" as we can't handle with our current
// scope modeling.
compiler.report(
JSError.make(n, VAR_ARGUMENTS_SHADOWED_ERROR));
}
}
public void removeDuplicates() {
for (Node n : dupDeclNodes) {
Node parent = n.getParent();
if (parent != null) {
n.detach();
compiler.reportChangeToEnclosingScope(parent);
}
}
}
}
/** Lazily create a "new" externs root for undeclared variables. */
private static Node getSynthesizedExternsRoot(AbstractCompiler compiler) {
return compiler.getSynthesizedExternsInput().getAstRoot(compiler);
}
}