/*
* Copyright 2007 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 com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashMap;
import java.util.Map;
/**
* @author nicksantos@google.com (Nick Santos)
*/
public final class ProcessDefinesTest extends TypeICompilerTestCase {
public ProcessDefinesTest() {
super(DEFAULT_EXTERNS + "var externMethod;");
// ProcessDefines emits warnings if the user tries to re-define a constant,
// but the constant is not defined anywhere in the binary.
allowSourcelessWarnings();
}
private Map<String, Node> overrides = new HashMap<>();
private GlobalNamespace namespace;
@Override
protected void setUp() throws Exception {
super.setUp();
overrides.clear();
}
@Override
protected CompilerPass getProcessor(Compiler compiler) {
return new ProcessDefinesWithInjectedNamespace(compiler);
}
@Override
protected int getNumRepetitions() {
// Only do one repetition, so that we can make sure the first pass keeps
// GlobalNamespace up to date.
return 1;
}
/**
* Helper for tests that expects definitions to remain unchanged, such
* that {@code definitions+js} is converted to {@code definitions+expected}.
*/
private void testWithPrefix(String definitions, String js, String expected) {
test(definitions + js, definitions + expected);
}
public void testBasicDefine1() {
test("/** @define {boolean} */ var DEF = true", "/** @define {boolean} */ var DEF=true");
}
public void testBasicDefine2() {
test("/** @define {string} */ var DEF = 'a'", "/** @define {string} */ var DEF=\"a\"");
}
public void testBasicDefine3() {
test("/** @define {number} */ var DEF = 0", "/** @define {number} */ var DEF=0");
}
public void testDefineBadType() {
testError("/** @define {Object} */ var DEF = {}", ProcessDefines.INVALID_DEFINE_TYPE_ERROR);
}
public void testDefineWithBadValue1() {
testError("/** @define {boolean} */ var DEF = new Boolean(true);",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testDefineWithBadValue2() {
testError("/** @define {string} */ var DEF = 'x' + y;",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testDefineWithDependentValue() {
test(
LINE_JOINER.join(
"/** @define {boolean} */ var BASE = false;",
"/** @define {boolean} */ var DEF = !BASE;"),
LINE_JOINER.join(
"/** @define {boolean} */ var BASE = false;",
"/** @define {boolean} */ var DEF = !BASE"));
test(
LINE_JOINER.join(
"var a = {};",
"/** @define {boolean} */ a.BASE = false;",
"/** @define {boolean} */ a.DEF = !a.BASE;"),
LINE_JOINER.join(
"var a={};",
"/** @define {boolean} */ a.BASE = false;",
"/** @define {boolean} */ a.DEF = !a.BASE"));
}
public void testDefineWithInvalidDependentValue() {
testError("var BASE = false;\n" +
"/** @define {boolean} */ var DEF = !BASE;",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testOverriding1() {
overrides.put("DEF_OVERRIDE_TO_TRUE", new Node(Token.TRUE));
overrides.put("DEF_OVERRIDE_TO_FALSE", new Node(Token.FALSE));
test(
"/** @define {boolean} */ var DEF_OVERRIDE_TO_TRUE = false;"
+ "/** @define {boolean} */ var DEF_OVERRIDE_TO_FALSE = true",
"/** @define {boolean} */ var DEF_OVERRIDE_TO_TRUE = true;"
+ "/** @define {boolean} */var DEF_OVERRIDE_TO_FALSE=false");
}
public void testOverriding2() {
overrides.put("DEF_OVERRIDE_TO_TRUE", new Node(Token.TRUE));
String normalConst = "var DEF_OVERRIDE_TO_FALSE=true;";
testWithPrefix(
normalConst,
"/** @define {boolean} */ var DEF_OVERRIDE_TO_TRUE = false",
"/** @define {boolean} */ var DEF_OVERRIDE_TO_TRUE = true");
}
public void testOverriding3() {
overrides.put("DEF_OVERRIDE_TO_TRUE", new Node(Token.TRUE));
test(
"/** @define {boolean} */ var DEF_OVERRIDE_TO_TRUE = true;",
"/** @define {boolean} */ var DEF_OVERRIDE_TO_TRUE = true");
}
public void testOverridingString0() {
test(
"/** @define {string} */ var DEF_OVERRIDE_STRING = 'x';",
"/** @define {string} */ var DEF_OVERRIDE_STRING=\"x\"");
}
public void testOverridingString1() {
test(
"/** @define {string} */ var DEF_OVERRIDE_STRING = 'x' + 'y';",
"/** @define {string} */ var DEF_OVERRIDE_STRING=\"x\" + \"y\"");
}
public void testOverridingString2() {
overrides.put("DEF_OVERRIDE_STRING", Node.newString("foo"));
test(
"/** @define {string} */ var DEF_OVERRIDE_STRING = 'x';",
"/** @define {string} */ var DEF_OVERRIDE_STRING=\"foo\"");
}
public void testOverridingString3() {
overrides.put("DEF_OVERRIDE_STRING", Node.newString("foo"));
test(
"/** @define {string} */ var DEF_OVERRIDE_STRING = 'x' + 'y';",
"/** @define {string} */ var DEF_OVERRIDE_STRING=\"foo\"");
}
public void testMisspelledOverride() {
overrides.put("DEF_BAD_OVERIDE", new Node(Token.TRUE));
test(
"/** @define {boolean} */ var DEF_BAD_OVERRIDE = true",
"/** @define {boolean} */ var DEF_BAD_OVERRIDE = true",
null,
ProcessDefines.UNKNOWN_DEFINE_WARNING);
}
public void testCompiledIsKnownDefine() {
overrides.put("COMPILED", new Node(Token.TRUE));
testSame("");
}
public void testSimpleReassign1() {
// Here and in other tests where we reassign to @defined values, we @suppress newCheckTypes
// to suppress NTI_CONST_REASSIGNED warnings.
test(
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {boolean} */ var DEF = false;",
"DEF = true;"),
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {boolean} */ var DEF=true;",
"true"));
}
public void testSimpleReassign2() {
test(
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {number|boolean} */ var DEF=false;",
"DEF=true;",
"DEF=3"),
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {number|boolean} */ var DEF=3;",
"true;3"));
Name def = namespace.getNameIndex().get("DEF");
assertThat(def.getRefs()).hasSize(1);
assertEquals(1, def.globalSets);
assertNotNull(def.getDeclaration());
}
public void testSimpleReassign3() {
test(
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {boolean} */ var DEF = false;",
"var x;",
"x = DEF = true;"),
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {boolean} */ var DEF = true;",
"var x;",
"x = true"));
}
public void testAssignBeforeDeclaration1() {
testError("DEF=false;var b=false,/** @define {boolean} */DEF=true,c=false",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testAssignBeforeDeclaration2() {
overrides.put("DEF_OVERRIDE_TO_TRUE", new Node(Token.TRUE));
testError(
"DEF_OVERRIDE_TO_TRUE = 3;" +
"/** @define {boolean|number} */ var DEF_OVERRIDE_TO_TRUE = false;",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testEmptyDeclaration() {
testError("/** @define {boolean} */ var DEF;", ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testReassignAfterCall() {
testError("/** @define {boolean} */var DEF=true;externMethod();DEF=false",
ProcessDefines.DEFINE_NOT_ASSIGNABLE_ERROR);
}
public void testReassignAfterRef() {
testError("/** @define {boolean} */var DEF=true;var x = DEF;DEF=false",
ProcessDefines.DEFINE_NOT_ASSIGNABLE_ERROR);
}
public void testReassignWithExpr() {
testError("/** @define {boolean} */var DEF=true;var x;DEF=x=false",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testReassignAfterNonGlobalRef() {
test(
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {boolean} */ var DEF=true;",
"var x=function(){var y=DEF};",
"DEF=false"),
LINE_JOINER.join(
"/** @fileoverview @suppress {newCheckTypes} */",
"/** @define {boolean} */ var DEF = false;",
"var x = function(){var y = DEF; };",
"false"));
Name def = namespace.getNameIndex().get("DEF");
assertThat(def.getRefs()).hasSize(2);
assertEquals(1, def.globalSets);
assertNotNull(def.getDeclaration());
}
public void testReassignAfterRefInConditional() {
testError(
"/** @define {boolean} */var DEF=true;" +
"if (false) {var x=DEF} DEF=false;",
ProcessDefines.DEFINE_NOT_ASSIGNABLE_ERROR);
}
public void testAssignInNonGlobalScope() {
testError("/** @define {boolean} */var DEF=true;function foo() {DEF=false};",
ProcessDefines.NON_GLOBAL_DEFINE_INIT_ERROR);
}
public void testDeclareInNonGlobalScope() {
testError("function foo() {/** @define {boolean} */var DEF=true;};",
ProcessDefines.NON_GLOBAL_DEFINE_INIT_ERROR);
}
public void testDefineAssignmentInLoop() {
testError("/** @define {boolean} */var DEF=true;var x=0;while (x) {DEF=false;}",
ProcessDefines.NON_GLOBAL_DEFINE_INIT_ERROR);
}
public void testWithNoDefines() {
testSame("var DEF=true;var x={};x.foo={}");
}
public void testNamespacedDefine1() {
test("var a = {}; /** @define {boolean} */ a.B = false; a.B = true;",
"var a = {}; /** @define {boolean} */ a.B = true; true;");
Name aDotB = namespace.getNameIndex().get("a.B");
assertThat(aDotB.getRefs()).hasSize(1);
assertEquals(1, aDotB.globalSets);
assertNotNull(aDotB.getDeclaration());
}
public void testNamespacedDefine2a() {
overrides.put("a.B", new Node(Token.TRUE));
test("var a = {}; /** @define {boolean} */ a.B = false;",
"var a = {}; /** @define {boolean} */ a.B = true;");
}
public void testNamespacedDefine2b() {
overrides.put("a.B", new Node(Token.TRUE));
testError(
"var a = { /** @define {boolean} */ B : false };",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testNamespacedDefine2c() {
overrides.put("a.B", new Node(Token.TRUE));
testError(
"var a = { /** @define {boolean} */ get B() { return false } };",
ProcessDefines.INVALID_DEFINE_INIT_ERROR);
}
public void testNamespacedDefine3() {
overrides.put("a.B", new Node(Token.TRUE));
test("var a = {};", "var a = {};", null,
ProcessDefines.UNKNOWN_DEFINE_WARNING);
}
public void testNamespacedDefine4() {
overrides.put("a.B", new Node(Token.TRUE));
test("var a = {}; /** @define {boolean} */ a.B = false;",
"var a = {}; /** @define {boolean} */ a.B = true;");
}
public void testOverrideAfterAlias() {
testError("var x; /** @define {boolean} */var DEF=true; x=DEF; DEF=false;",
ProcessDefines.DEFINE_NOT_ASSIGNABLE_ERROR);
}
private class ProcessDefinesWithInjectedNamespace implements CompilerPass {
private final Compiler compiler;
public ProcessDefinesWithInjectedNamespace(Compiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node js) {
namespace = new GlobalNamespace(compiler, js);
new ProcessDefines(compiler, overrides)
.injectNamespace(namespace)
.process(externs, js);
}
}
}