com.google.javascript.jscomp.VarCheck Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-linter Show documentation
Show all versions of closure-compiler-linter Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
This binary checks for style issues such as incorrect or missing JSDoc
usage, and missing goog.require() statements. It does not do more advanced
checks such as typechecking.
/*
* 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.common.collect.ImmutableSet;
import com.google.javascript.jscomp.Es6SyntacticScopeCreator.RedeclarationHandler;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
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 implements ScopedCallback, 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 BLOCK_SCOPED_DECL_MULTIPLY_DECLARED_ERROR =
DiagnosticType.error(
"JSC_BLOCK_SCOPED_DECL_MULTIPLY_DECLARED_ERROR",
"Duplicate let / const / class / function 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";
private static final Node googLoadModule = IR.getprop(IR.name("goog"), "loadModule");
private static final Node googProvide = IR.getprop(IR.name("goog"), "provide");
// 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 varsToDeclareInExterns = new LinkedHashSet<>();
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;
// All roots of goog.provided namespaces, e.g. 'a' given goog.provide('a.b.c');
private Set googProvidedRoots = new HashSet<>();
private final boolean closurePass;
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;
this.closurePass = compiler.getOptions() != null && compiler.getOptions().closurePass;
}
/**
* Creates the scope creator used by this pass. If not in validity check mode, use a {@link
* RedeclarationCheckHandler} to check var redeclarations.
*/
private Es6SyntacticScopeCreator 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();
if (closurePass) {
gatherImplicitVars(compiler.getRoot());
}
// 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(compiler, varName);
}
if (dupHandler != null) {
dupHandler.removeDuplicates();
}
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
checkState(scriptRoot.isScript());
if (closurePass) {
// Only run over the new script.
gatherImplicitVars(compiler.getRoot());
}
Es6SyntacticScopeCreator 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 boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName()) {
checkName(t, n, parent);
}
}
/** Validates that a NAME node does not refer to an undefined name. */
private void checkName(NodeTraversal t, Node n, Node parent) {
String varName = n.getString();
// Only a function can have an empty name.
if (varName.isEmpty()) {
// Name is optional for function expressions
// x = function() {...}
// Arrow functions are also expressions and cannot have a name
// x = () => {...}
// Member functions have an empty NAME node string, because the actual name is stored on the
// MEMBER_FUNCTION_DEF object that contains the FUNCTION.
// class C { foo() {...} }
// x = { foo() {...} }
checkState(NodeUtil.isFunctionExpression(parent) || NodeUtil.isMethodDeclaration(parent));
return;
}
Scope scope = t.getScope();
Var var = scope.getVar(varName);
Scope varScope = var != null ? var.getScope() : null;
// Check if this variable is reference in the externs, if so mark it as a duplicate.
if (varScope != null
&& varScope.isGlobal()
&& (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.
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 if (NodeUtil.isNonlocalModuleExportName(n)) {
// e.g. "export {a as b}" or "import {b as a} from './foo.js'
// where b is defined in a module's export entries but not in any module scope.
} else if (googProvidedRoots.contains(n.getString())) {
// Ignore names that have been defined by a goog.provide.
} else {
boolean isTypeOf = parent.isTypeOf();
// The extern checks are stricter, don't report a second error.
if (!isTypeOf && !(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);
}
}
}
}
private void gatherImplicitVars(Node root) {
GatherImplicitClosureGlobals closureGlobals = new GatherImplicitClosureGlobals();
NodeTraversal.traverse(compiler, root, closureGlobals);
googProvidedRoots = closureGlobals.roots.build();
}
/** Looks for goog.provided roots and legacy goog.modules (including in goog.loadModules). */
private static final class GatherImplicitClosureGlobals extends AbstractPreOrderCallback {
private final ImmutableSet.Builder roots = ImmutableSet.builder();
@Override
public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
// Don't traverse the entire AST. We just need to find goog.provides and legacy goog.modules.
switch (n.getToken()) {
case MODULE_BODY:
if (parent.getBooleanProp(Node.GOOG_MODULE)) {
addGoogModuleIfLegacy(n);
}
return false;
case EXPR_RESULT:
Node call = n.getOnlyChild();
if (!call.isCall()) {
return false;
}
Node target = call.getFirstChild();
Node arg = target.getNext();
if (arg == null) {
return false;
}
if (target.matchesQualifiedName(googProvide)) {
addRootNs(arg.getString());
} else if (target.matchesQualifiedName(googLoadModule) && arg.isFunction()) {
addGoogModuleIfLegacy(NodeUtil.getFunctionBody(arg));
}
return false;
case SCRIPT:
case ROOT:
return true;
default:
return false;
}
}
private void addGoogModuleIfLegacy(Node googModuleBody) {
Node googModuleCall = googModuleBody.getFirstChild();
if (googModuleCall == null || !NodeUtil.isExprCall(googModuleCall)) {
return; // This is bad code, but another pass reports the error.
}
Node legacyNamespace = googModuleCall.getNext();
if (legacyNamespace != null
&& NodeUtil.isGoogModuleDeclareLegacyNamespaceCall(legacyNamespace)) {
addRootNs(googModuleCall.getFirstChild().getSecondChild().getString());
}
}
private void addRootNs(String fullNs) {
int indexOfDot = fullNs.indexOf('.');
roots.add(indexOfDot == -1 ? fullNs : fullNs.substring(0, indexOfDot));
}
}
@Override
public void enterScope(NodeTraversal t) {}
@Override
public void exitScope(NodeTraversal t) {
if (!validityCheck && t.inGlobalScope()) {
Scope scope = t.getScope();
// Add symbols that are known to be needed to the standard injected code (polyfills, etc).
for (String requiredSymbol : REQUIRED_SYMBOLS) {
Var var = scope.getVar(requiredSymbol);
if (var == null) {
varsToDeclareInExterns.add(requiredSymbol);
}
}
}
}
/**
* List of symbols that must always be externed even if they are not referenced anywhere (yet).
* These are used by runtime libraries that might not be present when the first VarCheck runs.
*/
static final ImmutableSet REQUIRED_SYMBOLS =
ImmutableSet.of(
"Array",
"Error",
"Float32Array",
"Function",
"Infinity",
"Map",
"Math",
"Number",
"Object",
"Promise",
"RangeError",
"Reflect",
"RegExp",
"Set",
"String",
"Symbol",
"TypeError",
"WeakMap",
"global",
"isNaN",
"parseFloat",
"parseInt",
"self",
"undefined",
"window");
/**
* 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 implements Callback {
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
// Type summaries are generated from code rather than hand-written,
// so warning about name references there would usually not be helpful.
return !n.isScript() || !NodeUtil.isFromTypeSummary(n);
}
@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) {
if (!googProvidedRoots.contains(n.getString())) {
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());
}
}
}
}
/** Returns true if duplication warnings are suppressed on either n or origVar. */
static boolean hasDuplicateDeclarationSuppression(
AbstractCompiler compiler, Node n, Node origVar) {
// For VarCheck and VariableReferenceCheck, variables in externs do not generate duplicate
// warnings.
if (isExternNamespace(n)) {
return true;
}
return TypeValidator.hasDuplicateDeclarationSuppression(compiler, origVar);
}
/** Returns true if n is the name of a variable that declares a namespace in an externs file. */
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 final ArrayList dupDeclNodes = new ArrayList<>();
@Override
public void onRedeclaration(
Scope s, String name, Node n, CompilerInput input) {
Node parent = NodeUtil.getDeclaringParent(n);
Var origVar = s.getVar(name);
// origNode will be null for `arguments`, since there's no node that declares it.
Node origNode = origVar.getNode();
Node origParent = (origNode == null) ? null : NodeUtil.getDeclaringParent(origNode);
if (parent.isLet()
|| parent.isConst()
|| parent.isClass()
|| (origParent != null
&& (origParent.isLet() || origParent.isConst() || origParent.isClass()))) {
compiler.report(JSError.make(n, BLOCK_SCOPED_DECL_MULTIPLY_DECLARED_ERROR));
return;
} else if (parent.isFunction()
// Redeclarations of functions in global scope are fairly common, so allow them
// (at least for now).
&& !s.isGlobal()
&& origParent != null
&& (origParent.isFunction()
|| origParent.isLet()
|| origParent.isConst()
|| origParent.isClass())) {
compiler.report(JSError.make(n, BLOCK_SCOPED_DECL_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(compiler, n, origVar.getNameNode());
if (VarCheck.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);
}
}