/* * 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(); } } }); } }