/*
* Copyright 2005 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.truth.Truth.assertThat;
import static com.google.javascript.jscomp.VarCheck.VAR_MULTIPLY_DECLARED_ERROR;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;
public final class VarCheckTest extends Es6CompilerTestCase {
private static final String EXTERNS = "var window; function alert() {}";
private CheckLevel strictModuleDepErrorLevel;
private boolean sanityCheck = false;
private CheckLevel externValidationErrorLevel;
private boolean declarationCheck;
public VarCheckTest() {
super(EXTERNS);
}
@Override
protected void setUp() throws Exception {
super.setUp();
// Setup value set by individual tests to the appropriate defaults.
super.allowExternsChanges(true);
strictModuleDepErrorLevel = CheckLevel.OFF;
externValidationErrorLevel = null;
sanityCheck = false;
declarationCheck = false;
}
@Override
protected CompilerOptions getOptions() {
CompilerOptions options = super.getOptions();
options.setWarningLevel(DiagnosticGroups.STRICT_MODULE_DEP_CHECK,
strictModuleDepErrorLevel);
if (externValidationErrorLevel != null) {
options.setWarningLevel(DiagnosticGroups.EXTERNS_VALIDATION,
externValidationErrorLevel);
}
return options;
}
@Override
protected CompilerPass getProcessor(final Compiler compiler) {
return new CompilerPass() {
@Override public void process(Node externs, Node root) {
new VarCheck(compiler, sanityCheck).process(externs, root);
if (!sanityCheck && !compiler.hasErrors()) {
// If the original test turned off sanity check, make sure our synthesized
// code passes it.
new VarCheck(compiler, true).process(externs, root);
}
if (declarationCheck) {
new VariableTestCheck(compiler).process(externs, root);
}
}
};
}
@Override
protected int getNumRepetitions() {
// Because we synthesize externs, the second pass won't emit a warning.
return 1;
}
public void testShorthandObjLit() {
testErrorEs6("var x = {y};", VarCheck.UNDEFINED_VAR_ERROR);
testSameEs6("var {x} = {x: 5}; let y = x;");
}
public void testBreak() {
testSame("a: while(1) break a;");
}
public void testContinue() {
testSame("a: while(1) continue a;");
}
public void testReferencedVarNotDefined() {
testError("x = 0;", VarCheck.UNDEFINED_VAR_ERROR);
}
public void testReferencedLetNotDefined() {
testErrorEs6("{ let x = 1; } var y = x;", VarCheck.UNDEFINED_VAR_ERROR);
}
public void testReferencedLetDefined1() {
testSameEs6("let x; x = 1;");
}
public void testReferencedLetDefined2() {
testSameEs6("let x; function y() {x = 1;}");
}
public void testReferencedConstDefined2() {
testSameEs6("const x = 1; var y = x + 1;");
}
public void testReferencedVarDefined1() {
testSame("var x, y; x=1;");
}
public void testReferencedVarDefined2() {
testSame("var x; function y() {x=1;}");
}
public void testReferencedVarsExternallyDefined() {
testSame("var x = window; alert(x);");
}
public void testMultiplyDeclaredVars1() {
testError("var x = 1; var x = 2;", VarCheck.VAR_MULTIPLY_DECLARED_ERROR);
}
public void testMultiplyDeclaredVars2() {
testSame("var y; try { y=1 } catch (x) {} try { y=1 } catch (x) {}");
}
public void testMultiplyDeclaredVars3() {
testSame("try { var x = 1; x *=2; } catch (x) {}");
}
public void testMultiplyDeclaredVars4() {
testSame("x;", "var x = 1; var x = 2;", VAR_MULTIPLY_DECLARED_ERROR, true);
}
public void testMultiplyDeclaredLets() {
testErrorEs6("let x = 1; let x = 2;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("let x = 1; var x = 2;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("var x = 1; let x = 2;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
}
public void testMultiplyDeclaredConsts() {
testErrorEs6("const x = 1; const x = 2;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("const x = 1; var x = 2;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("var x = 1; const x = 2;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
}
public void testMultiplyDeclareLetsInDifferentScope() {
testSameEs6("let x = 1; if (123) {let x = 2;}");
testSameEs6("try {let x = 1;} catch(x){}");
}
public void testReferencedVarDefinedClass() {
testErrorEs6("var x; class x{ }", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("let x; class x{ }", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("const x = 1; class x{ }", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6("class x{ } let x;", VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
}
public void testNamedClass() {
testSameEs6("class x {}");
testSameEs6("var x = class x {};");
testSameEs6("var y = class x {};");
testSameEs6("var y = class x { foo() { return new x; } };");
testErrorEs6("var Foo = class extends Bar {};", VarCheck.UNDEFINED_VAR_ERROR);
}
public void testVarReferenceInExterns() {
testSame("asdf;", "var /** @suppress {duplicate} */ asdf;",
VarCheck.NAME_REFERENCE_IN_EXTERNS_ERROR);
}
public void testCallInExterns() {
testSame("yz();", "function /** @suppress {duplicate} */ yz() {}",
VarCheck.NAME_REFERENCE_IN_EXTERNS_ERROR);
}
public void testVarDeclarationInExterns() {
testSame("var asdf;", "asdf;", null);
}
public void testFunctionDeclarationInExterns() {
testSameEs6("function foo(x = 7) {}", "foo();", null);
testSameEs6("function foo(...rest) {}", "foo(1,2,3);", null);
}
public void testVarAssignmentInExterns() {
testSame("/** @type{{foo:string}} */ var foo; var asdf = foo;", "asdf.foo;", null);
}
public void testAliasesInExterns() {
externValidationErrorLevel = CheckLevel.ERROR;
testSame("var foo; /** @const */ var asdf = foo;", "", null);
testSame(
"var Foo; var ns = {}; /** @const */ ns.FooAlias = Foo;", "", null);
testSame(
LINE_JOINER.join(
"var ns = {}; /** @constructor */ ns.Foo = function() {};",
"var ns2 = {}; /** @const */ ns2.Bar = ns.Foo;"),
"", null);
}
public void testDuplicateNamespaceInExterns() {
testExternChanges(
"/** @const */ var ns = {}; /** @const */ var ns = {};",
"",
"/** @const */ var ns = {};");
}
public void testLetDeclarationInExterns() {
testSameEs6("let asdf;", "asdf;", null);
}
public void testConstDeclarationInExterns() {
testSameEs6("const asdf = 1;", "asdf;", null);
}
public void testNewInExterns() {
// Class is not hoisted.
testSameEs6("x = new Klass();", "class Klass{}",
VarCheck.UNDEFINED_VAR_ERROR, true);
}
public void testPropReferenceInExterns1() {
testSame("asdf.foo;", "var /** @suppress {duplicate} */ asdf;",
VarCheck.UNDEFINED_EXTERN_VAR_ERROR);
}
public void testPropReferenceInExterns2() {
testSame("asdf.foo;", "",
VarCheck.UNDEFINED_VAR_ERROR, true);
}
public void testPropReferenceInExterns3() {
testSame("asdf.foo;", "var /** @suppress {duplicate} */ asdf;",
VarCheck.UNDEFINED_EXTERN_VAR_ERROR);
externValidationErrorLevel = CheckLevel.ERROR;
testSame(
"asdf.foo;", "var asdf;",
VarCheck.UNDEFINED_EXTERN_VAR_ERROR, true);
externValidationErrorLevel = CheckLevel.OFF;
test("asdf.foo;", "var asdf;", "var /** @suppress {duplicate} */ asdf;", null, null);
}
public void testPropReferenceInExterns4() {
testSameEs6("asdf.foo;", "let asdf;",
VarCheck.UNDEFINED_EXTERN_VAR_ERROR);
}
public void testPropReferenceInExterns5() {
testSameEs6("asdf.foo;", "class asdf {}",
VarCheck.UNDEFINED_EXTERN_VAR_ERROR);
}
public void testVarInWithBlock() {
testError("var a = {b:5}; with (a){b;}", VarCheck.UNDEFINED_VAR_ERROR);
}
public void testFunctionDeclaredInBlock() {
testError("if (true) {function foo() {}} foo();", VarCheck.UNDEFINED_VAR_ERROR);
testError("foo(); if (true) {function foo() {}}", VarCheck.UNDEFINED_VAR_ERROR);
testSameEs6("if (true) {var foo = ()=>{}} foo();");
testErrorEs6("if (true) {let foo = ()=>{}} foo();", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("if (true) {const foo = ()=>{}} foo();", VarCheck.UNDEFINED_VAR_ERROR);
testSameEs6("foo(); if (true) {var foo = ()=>{}}");
testErrorEs6("foo(); if (true) {let foo = ()=>{}}", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("foo(); if (true) {const foo = ()=>{}}", VarCheck.UNDEFINED_VAR_ERROR);
}
public void testValidFunctionExpr() {
testSame("(function() {});");
}
public void testRecursiveFunction() {
testSame("(function a() { return a(); })();");
}
public void testRecursiveFunction2() {
testSame("var a = 3; (function a() { return a(); })();");
}
public void testParam() {
testSame("function fn(a){ var b = a; }");
testSame("function fn(a){ var a = 2; }");
testError("function fn(){ var b = a; }", VarCheck.UNDEFINED_VAR_ERROR);
// Default parameters
testErrorEs6(
"function fn(a = b) { function g(a = 3) { var b; } }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x=a) { let a; }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x=a) { { let a; } }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x=b) { function a(x=1) { var b; } }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x=a) { var a; }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x=a()) { function a() {} }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x=[a]) { var a; }", VarCheck.UNDEFINED_VAR_ERROR);
testErrorEs6("function f(x = new foo.bar()) {}", VarCheck.UNDEFINED_VAR_ERROR);
testSameEs6("var foo = {}; foo.bar = class {}; function f(x = new foo.bar()) {}");
testSameEs6("function fn(a = 2){ var b = a; }");
testSameEs6("function fn(a = 2){ var a = 3; }");
testSameEs6("function fn({a, b}){ var c = a; }");
testSameEs6("function fn({a, b}){ var a = 3; }");
}
public void testLegalVarReferenceBetweenModules() {
testDependentModules("var x = 10;", "var y = x++;", null);
}
public void testLegalLetReferenceBetweenModules() {
setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
testDependentModules("let x = 10;", "let y = x++;", null);
}
public void testLegalConstReferenceBetweenModules() {
setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
testDependentModules("const x = 10;", "const y = x + 1;", null);
}
public void testMissingModuleDependencyDefault() {
testIndependentModules("var x = 10;", "var y = x++;",
null, VarCheck.MISSING_MODULE_DEP_ERROR);
}
public void testMissingModuleDependencyLetAndConst() {
setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
testIndependentModules("let x = 10;", "let y = x++;",
null, VarCheck.MISSING_MODULE_DEP_ERROR);
testIndependentModules("const x = 10;", "const y = x + 1;",
null, VarCheck.MISSING_MODULE_DEP_ERROR);
}
public void testViolatedModuleDependencyDefault() {
testDependentModules("var y = x++;", "var x = 10;",
VarCheck.VIOLATED_MODULE_DEP_ERROR);
}
public void testViolatedModuleDependencyLetAndConst() {
setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
testDependentModules("let y = x++;", "let x = 10;",
VarCheck.VIOLATED_MODULE_DEP_ERROR);
testDependentModules("const y = x + 1;", "const x = 10;",
VarCheck.VIOLATED_MODULE_DEP_ERROR);
}
public void testMissingModuleDependencySkipNonStrict() {
sanityCheck = true;
testIndependentModules("var x = 10;", "var y = x++;",
null, null);
}
public void testViolatedModuleDependencySkipNonStrict() {
sanityCheck = true;
testDependentModules("var y = x++;", "var x = 10;",
null);
}
public void testMissingModuleDependencySkipNonStrictNotPromoted() {
sanityCheck = true;
strictModuleDepErrorLevel = CheckLevel.ERROR;
testIndependentModules("var x = 10;", "var y = x++;", null, null);
}
public void testViolatedModuleDependencyNonStrictNotPromoted() {
sanityCheck = true;
strictModuleDepErrorLevel = CheckLevel.ERROR;
testDependentModules("var y = x++;", "var x = 10;", null);
}
public void testDependentStrictModuleDependencyCheck() {
strictModuleDepErrorLevel = CheckLevel.ERROR;
testDependentModules("var f = function() {return new B();};",
"var B = function() {}",
VarCheck.STRICT_MODULE_DEP_ERROR);
}
public void testIndependentStrictModuleDependencyCheck() {
strictModuleDepErrorLevel = CheckLevel.ERROR;
testIndependentModules("var f = function() {return new B();};",
"var B = function() {}",
VarCheck.STRICT_MODULE_DEP_ERROR, null);
}
public void testStarStrictModuleDependencyCheck() {
strictModuleDepErrorLevel = CheckLevel.WARNING;
testSame(createModuleStar("function a() {}", "function b() { a(); c(); }",
"function c() { a(); }"),
VarCheck.STRICT_MODULE_DEP_ERROR);
}
public void testForwardVarReferenceInLocalScope1() {
testDependentModules("var x = 10; function a() {y++;}",
"var y = 11; a();", null);
}
public void testForwardVarReferenceInLocalScope2() {
// It would be nice if this pass could use a call graph to flag this case
// as an error, but it currently doesn't.
testDependentModules("var x = 10; function a() {y++;} a();",
"var y = 11;", null);
}
private void testDependentModules(String code1, String code2,
DiagnosticType error) {
testDependentModules(code1, code2, error, null);
}
private void testDependentModules(String code1, String code2,
DiagnosticType error,
DiagnosticType warning) {
testTwoModules(code1, code2, true, error, warning);
}
private void testIndependentModules(String code1, String code2,
DiagnosticType error,
DiagnosticType warning) {
testTwoModules(code1, code2, false, error, warning);
}
private void testTwoModules(String code1, String code2, boolean m2DependsOnm1,
DiagnosticType error, DiagnosticType warning) {
JSModule m1 = new JSModule("m1");
m1.add(SourceFile.fromCode("input1", code1));
JSModule m2 = new JSModule("m2");
m2.add(SourceFile.fromCode("input2", code2));
if (m2DependsOnm1) {
m2.addDependency(m1);
}
if (error == null) {
test(new JSModule[] { m1, m2 },
new String[] { code1, code2 }, null, warning);
} else {
test(new JSModule[] { m1, m2 },
null, error, warning);
}
}
//////////////////////////////////////////////////////////////////////////////
// Test synthesis of externs
public void testSimple() {
checkSynthesizedExtern("x", "var x;");
checkSynthesizedExtern("var x", "");
}
public void testSimpleSanityCheck() {
sanityCheck = true;
try {
checkSynthesizedExtern("x", "");
fail("Expected RuntimeException");
} catch (RuntimeException e) {
assertThat(e.getMessage()).contains("Unexpected variable x");
}
}
public void testParameter() {
checkSynthesizedExtern("function f(x){}", "");
}
public void testLocalVar() {
checkSynthesizedExtern("function f(){x}", "var x");
}
public void testTwoLocalVars() {
checkSynthesizedExtern("function f(){x}function g() {x}", "var x");
}
public void testInnerFunctionLocalVar() {
checkSynthesizedExtern("function f(){function g() {x}}", "var x");
}
public void testNoCreateVarsForLabels() {
checkSynthesizedExtern("x:var y", "");
}
public void testVariableInNormalCodeUsedInExterns1() {
checkSynthesizedExtern(
"x.foo;", "var x;", "var x; x.foo;");
}
public void testVariableInNormalCodeUsedInExterns2() {
checkSynthesizedExtern(
"x;", "var x;", "var x; x;");
}
public void testVariableInNormalCodeUsedInExterns3() {
checkSynthesizedExtern(
"x.foo;", "function x() {}", "var x; x.foo; ");
}
public void testVariableInNormalCodeUsedInExterns4() {
checkSynthesizedExtern(
"x;", "function x() {}", "var x; x; ");
}
public void testRedeclaration1() {
String js = "var a; var a;";
testError(js, VarCheck.VAR_MULTIPLY_DECLARED_ERROR);
}
public void testRedeclaration2() {
String js = "var a; /** @suppress {duplicate} */ var a;";
testSame(js);
}
public void testRedeclaration3() {
String js = " /** @suppress {duplicate} */ var a; var a; ";
testSame(js);
}
public void testSuppressionWithInlineJsDoc() {
testSame("/** @suppress {duplicate} */ var /** number */ a; var a;");
}
public void testDuplicateVar() {
testError("/** @define {boolean} */ var DEF = false; var DEF = true;",
VAR_MULTIPLY_DECLARED_ERROR);
}
public void testDontAllowSuppressDupeOnLet() {
testErrorEs6(
"let a; /** @suppress {duplicate} */ let a; ",
VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6(
"function f() { let a; /** @suppress {duplicate} */ let a; }",
VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
}
public void testDuplicateBlockScopedDeclarationInSwitch() {
testErrorEs6(
LINE_JOINER.join(
"function f(x) {",
" switch (x) {",
" case 'a':",
" let z = 123;",
" break;",
" case 'b':",
" let z = 234;",
" break;",
" }",
"}"),
VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
testErrorEs6(
LINE_JOINER.join(
"function f(x) {",
" switch (x) {",
" case 'a':",
" class C {}",
" break;",
" case 'b':",
" class C {}",
" break;",
" }",
"}"),
VarCheck.LET_CONST_CLASS_MULTIPLY_DECLARED_ERROR);
}
public void testFunctionScopeArguments() {
// A var declaration doesn't mask arguments
testSame("function f() {var arguments}");
testError("var f = function arguments() {}", VarCheck.VAR_ARGUMENTS_SHADOWED_ERROR);
testError("var f = function (arguments) {}", VarCheck.VAR_ARGUMENTS_SHADOWED_ERROR);
testSame("function f() {try {} catch(arguments) {}}");
sanityCheck = true;
testSame("function f() {var arguments}");
}
public void testNoUndeclaredVarWhenUsingClosurePass() {
enableClosurePass();
// We don't want to get goog as an undeclared var here.
testError("goog.require('namespace.Class1');\n",
ProcessClosurePrimitives.MISSING_PROVIDE_ERROR);
}
private static final class VariableTestCheck implements CompilerPass {
final AbstractCompiler compiler;
VariableTestCheck(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseRootsEs6(compiler,
new AbstractPostOrderCallback() {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName() && !parent.isFunction()
&& !parent.isLabel()) {
assertTrue("Variable " + n.getString() + " should have be declared",
t.getScope().isDeclared(n.getString(), true));
}
}
},
externs, root);
}
}
public void checkSynthesizedExtern(
String input, String expectedExtern) {
checkSynthesizedExtern("", input, expectedExtern);
}
public void checkSynthesizedExtern(
String extern, String input, String expectedExtern) {
declarationCheck = !sanityCheck;
this.enableCompareAsTree(false);
testExternChanges(extern, input, expectedExtern);
}
}