/*
* Copyright 2017 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.CompilerOptions.LanguageMode.ECMASCRIPT_NEXT;
import static com.google.javascript.jscomp.testing.NodeSubject.assertNode;
import com.google.javascript.jscomp.ReferenceCollectingCallback.Behavior;
import com.google.javascript.rhino.Token;
public final class ReferenceCollectingCallbackTest extends CompilerTestCase {
private Behavior behavior;
private boolean es6ScopeCreator;
@Override
public void setUp() {
setLanguage(ECMASCRIPT_NEXT, ECMASCRIPT_NEXT);
behavior = null;
es6ScopeCreator = true;
}
@Override
protected int getNumRepetitions() {
// Default behavior for CompilerTestCase.test*() methods is to do the whole test twice,
// because passes that modify the AST need to be idempotent.
// Since ReferenceCollectingCallback() just gathers information, it doesn't make sense to
// run it twice, and doing so just complicates debugging test cases.
return 1;
}
@Override
protected CompilerPass getProcessor(final Compiler compiler) {
ScopeCreator scopeCreator =
es6ScopeCreator
? new Es6SyntacticScopeCreator(compiler)
: SyntacticScopeCreator.makeUntyped(compiler);
return new ReferenceCollectingCallback(
compiler,
this.behavior,
scopeCreator);
}
private void testBehavior(String js, Behavior behavior) {
this.behavior = behavior;
testSame(js);
}
public void testVarInBlock_oldScopeCreator() {
es6ScopeCreator = false;
testBehavior(
LINE_JOINER.join(
"function f(x) {",
" if (true) {",
" var y = x;",
" y;",
" y;",
" }",
"}"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isFunctionScope()) {
ReferenceCollection y = rm.getReferences(t.getScope().getVar("y"));
assertThat(y.isAssignedOnceInLifetime()).isTrue();
assertThat(y.isWellDefined()).isTrue();
}
}
});
}
public void testVarInBlock() {
testBehavior(
LINE_JOINER.join(
"function f(x) {",
" if (true) {",
" var y = x;",
" y;",
" y;",
" }",
"}"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isBlockScope() && t.getScope().getParent().isFunctionBlockScope()) {
ReferenceCollection y = rm.getReferences(t.getScope().getVar("y"));
assertThat(y.isAssignedOnceInLifetime()).isTrue();
assertThat(y.isWellDefined()).isTrue();
}
}
});
}
public void testVarInLoopNotAssignedOnlyOnceInLifetime() {
Behavior behavior =
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isBlockScope()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.isAssignedOnceInLifetime()).isFalse();
}
}
};
testBehavior("while (true) { var x = 0; }", behavior);
testBehavior("while (true) { let x = 0; }", behavior);
}
/**
* Although there is only one assignment to x in the code, it's in a function which could be
* called multiple times, so {@code isAssignedOnceInLifetime()} returns false.
*/
public void testVarInFunctionNotAssignedOnlyOnceInLifetime() {
Behavior behavior =
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isGlobal()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.isAssignedOnceInLifetime()).isFalse();
}
}
};
testBehavior("var x; function f() { x = 0; }", behavior);
testBehavior("let x; function f() { x = 0; }", behavior);
}
public void testParameterAssignedOnlyOnceInLifetime() {
testBehavior(
"function f(x) { x; }",
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isFunctionScope()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.isAssignedOnceInLifetime()).isTrue();
}
}
});
}
public void testModifiedParameterNotAssignedOnlyOnceInLifetime() {
testBehavior(
"function f(x) { x = 3; }",
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isFunctionScope()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.isAssignedOnceInLifetime()).isFalse();
}
}
});
}
public void testVarAssignedOnceInLifetime1() {
Behavior behavior =
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isFunctionBlockScope()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.isAssignedOnceInLifetime()).isTrue();
}
}
};
testBehavior("function f() { var x = 0; }", behavior);
testBehavior("function f() { let x = 0; }", behavior);
}
public void testVarAssignedOnceInLifetime2() {
testBehavior(
"function f() { { let x = 0; } }",
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isBlockScope() && !t.getScope().isFunctionBlockScope()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.isAssignedOnceInLifetime()).isTrue();
}
}
});
}
public void testVarAssignedOnceInLifetime3() {
Behavior behavior =
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isCatchScope()) {
ReferenceCollection e = rm.getReferences(t.getScope().getVar("e"));
assertThat(e.isAssignedOnceInLifetime()).isTrue();
ReferenceCollection y = rm.getReferences(t.getScope().getVar("y"));
assertThat(y.isAssignedOnceInLifetime()).isTrue();
assertThat(y.isWellDefined()).isTrue();
}
}
};
testBehavior(
LINE_JOINER.join(
"try {",
"} catch (e) {",
" var y = e;",
" g();",
" y;y;",
"}"),
behavior);
testBehavior(
LINE_JOINER.join(
"try {",
"} catch (e) {",
" var y; y = e;",
" g();",
" y;y;",
"}"),
behavior);
}
public void testLetAssignedOnceInLifetime1() {
testBehavior(
LINE_JOINER.join(
"try {",
"} catch (e) {",
" let y = e;",
" g();",
" y;y;",
"}"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isCatchScope()) {
ReferenceCollection e = rm.getReferences(t.getScope().getVar("e"));
assertThat(e.isAssignedOnceInLifetime()).isTrue();
ReferenceCollection y = rm.getReferences(t.getScope().getVar("y"));
assertThat(y.isAssignedOnceInLifetime()).isTrue();
assertThat(y.isWellDefined()).isTrue();
}
}
});
}
public void testLetAssignedOnceInLifetime2() {
testBehavior(
LINE_JOINER.join(
"try {",
"} catch (e) {",
" let y; y = e;",
" g();",
" y;y;",
"}"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isCatchScope()) {
ReferenceCollection e = rm.getReferences(t.getScope().getVar("e"));
assertThat(e.isAssignedOnceInLifetime()).isTrue();
ReferenceCollection y = rm.getReferences(t.getScope().getVar("y"));
assertThat(y.isAssignedOnceInLifetime()).isTrue();
assertThat(y.isWellDefined()).isTrue();
}
}
});
}
public void testBasicBlocks() {
testBasicBlocks(true);
testBasicBlocks(false);
}
private void testBasicBlocks(boolean scopeCreator) {
es6ScopeCreator = scopeCreator;
testBehavior(
LINE_JOINER.join(
"var x = 0;",
"switch (x) {",
" case 0:",
" x;",
"}"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isGlobal()) {
ReferenceCollection x = rm.getReferences(t.getScope().getVar("x"));
assertThat(x.references).hasSize(3);
assertNode(x.references.get(0).getBasicBlock().getRoot()).hasType(Token.ROOT);
assertNode(x.references.get(1).getBasicBlock().getRoot()).hasType(Token.ROOT);
assertNode(x.references.get(2).getBasicBlock().getRoot()).hasType(Token.CASE);
}
}
});
}
public void testThis() {
testBehavior(
LINE_JOINER.join(
"/** @constructor */",
"function C() {}",
"",
"C.prototype.m = function m() {",
" var self = this;",
" if (true) {",
" alert(self);",
" }",
"};"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isFunctionBlockScope()
&& t.getScopeRoot().getParent().getFirstChild().matchesQualifiedName("m")) {
ReferenceCollection self = rm.getReferences(t.getScope().getVar("self"));
assertThat(self.isEscaped()).isFalse();
}
}
});
}
public void testThis_oldScopeCreator() {
es6ScopeCreator = false;
testBehavior(
LINE_JOINER.join(
"/** @constructor */",
"function C() {}",
"",
"C.prototype.m = function m() {",
" var self = this;",
" if (true) {",
" alert(self);",
" }",
"};"),
new Behavior() {
@Override
public void afterExitScope(NodeTraversal t, ReferenceMap rm) {
if (t.getScope().isFunctionBlockScope()
&& t.getScopeRoot().getParent().getFirstChild().matchesQualifiedName("m")) {
ReferenceCollection self = rm.getReferences(t.getScope().getVar("self"));
assertThat(self.isEscaped()).isFalse();
}
}
});
}
}