/* * Copyright 2008 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 com.google.javascript.jscomp.DataFlowAnalysis.FlowState; import com.google.javascript.jscomp.Scope.Var; import com.google.javascript.rhino.InputId; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import junit.framework.TestCase; /** * Tests for {@link LiveVariablesAnalysis}. Test cases are snippets of a * function and assertions are made at the instruction labeled with {@code X}. * */ public class LiveVariableAnalysisTest extends TestCase { private LiveVariablesAnalysis liveness = null; public void testStraightLine() { // A sample of simple straight line of code with different liveness changes. assertNotLiveBeforeX("X:var a;", "a"); assertNotLiveAfterX("X:var a;", "a"); assertNotLiveAfterX("X:var a=1;", "a"); assertLiveAfterX("X:var a=1; a()", "a"); assertNotLiveBeforeX("X:var a=1; a()", "a"); assertLiveBeforeX("var a;X:a;", "a"); assertLiveBeforeX("var a;X:a=a+1;", "a"); assertLiveBeforeX("var a;X:a+=1;", "a"); assertLiveBeforeX("var a;X:a++;", "a"); assertNotLiveAfterX("var a,b;X:b();", "a"); assertNotLiveBeforeX("var a,b;X:b();", "a"); assertLiveBeforeX("var a,b;X:b(a);", "a"); assertLiveBeforeX("var a,b;X:b(1,2,3,b(a + 1));", "a"); assertNotLiveBeforeX("var a,b;X:a=1;b(a)", "a"); assertNotLiveAfterX("var a,b;X:b(a);b()", "a"); assertLiveBeforeX("var a,b;X:b();b=1;a()", "b"); assertLiveAfterX("X:a();var a;a()", "a"); assertNotLiveAfterX("X:a();var a=1;a()", "a"); assertLiveBeforeX("var a,b;X:a,b=1", "a"); } public void testProperties() { // Reading property of a local variable makes that variable live. assertLiveBeforeX("var a,b;X:a.P;", "a"); // Assigning to a property doesn't kill "a". It makes it live instead. assertLiveBeforeX("var a,b;X:a.P=1;b()", "a"); assertLiveBeforeX("var a,b;X:a.P.Q=1;b()", "a"); // An "a" in a different context. assertNotLiveAfterX("var a,b;X:b.P.Q.a=1;", "a"); assertLiveBeforeX("var a,b;X:b.P.Q=a;", "a"); } public void testConditions() { // Reading the condition makes the variable live. assertLiveBeforeX("var a,b;X:if(a){}", "a"); assertLiveBeforeX("var a,b;X:if(a||b) {}", "a"); assertLiveBeforeX("var a,b;X:if(b||a) {}", "a"); assertLiveBeforeX("var a,b;X:if(b||b(a)) {}", "a"); assertNotLiveAfterX("var a,b;X:b();if(a) {}", "b"); // We can kill within a condition as well. assertNotLiveAfterX("var a,b;X:a();if(a=b){}a()", "a"); assertNotLiveAfterX("var a,b;X:a();while(a=b){}a()", "a"); // The kill can be "conditional" due to short circuit. assertNotLiveAfterX("var a,b;X:a();if((a=b)&&b){}a()", "a"); assertNotLiveAfterX("var a,b;X:a();while((a=b)&&b){}a()", "a"); assertLiveBeforeX("var a,b;a();X:if(b&&(a=b)){}a()", "a"); // Assumed live. assertLiveBeforeX("var a,b;a();X:if(a&&(a=b)){}a()", "a"); assertLiveBeforeX("var a,b;a();X:while(b&&(a=b)){}a()", "a"); assertLiveBeforeX("var a,b;a();X:while(a&&(a=b)){}a()", "a"); } public void testArrays() { assertLiveBeforeX("var a;X:a[1]", "a"); assertLiveBeforeX("var a,b;X:b[a]", "a"); assertLiveBeforeX("var a,b;X:b[1,2,3,4,b(a)]", "a"); assertLiveBeforeX("var a,b;X:b=[a,'a']", "a"); assertNotLiveBeforeX("var a,b;X:a=[];b(a)", "a"); // Element assignment doesn't kill the array. assertLiveBeforeX("var a;X:a[1]=1", "a"); } public void testTwoPaths() { // Both Paths. assertLiveBeforeX("var a,b;X:if(b){b(a)}else{b(a)};", "a"); // Only one path. assertLiveBeforeX("var a,b;X:if(b){b(b)}else{b(a)};", "a"); assertLiveBeforeX("var a,b;X:if(b){b(a)}else{b(b)};", "a"); // None of the paths. assertNotLiveAfterX("var a,b;X:if(b){b(b)}else{b(b)};", "a"); // At the very end. assertLiveBeforeX("var a,b;X:if(b){b(b)}else{b(b)}a();", "a"); // The loop might or might not be executed. assertLiveBeforeX("var a;X:while(param1){a()};", "a"); assertLiveBeforeX("var a;X:while(param1){a=1};a()", "a"); // Same idea with if. assertLiveBeforeX("var a;X:if(param1){a()};", "a"); assertLiveBeforeX("var a;X:if(param1){a=1};a()", "a"); // This is different in DO. We know for sure at least one iteration is // executed. assertNotLiveAfterX("X:var a;do{a=1}while(param1);a()", "a"); } public void testThreePaths() { assertLiveBeforeX("var a;X:if(1){}else if(2){}else{a()};", "a"); assertLiveBeforeX("var a;X:if(1){}else if(2){a()}else{};", "a"); assertLiveBeforeX("var a;X:if(1){a()}else if(2){}else{};", "a"); assertLiveBeforeX("var a;X:if(1){}else if(2){}else{};a()", "a"); } public void testHooks() { assertLiveBeforeX("var a;X:1?a=1:1;a()", "a"); // Unfortunately, we cannot prove the following because we assume there is // no control flow within a hook (i.e. no joins / set unions). // assertNotLiveAfterX("var a;X:1?a=1:a=2;a", "a"); assertLiveBeforeX("var a,b;X:b=1?a:2", "a"); } public void testForLoops() { // Induction variable should not be live after the loop. assertNotLiveBeforeX("var a,b;for(a=0;a<9;a++){b(a)};X:b", "a"); assertNotLiveBeforeX("var a,b;for(a in b){a()};X:b", "a"); assertNotLiveBeforeX("var a,b;for(a in b){a()};X:a", "b"); assertLiveBeforeX("var b;for(var a in b){X:a()};", "a"); // It should be live within the loop even if it is not used. assertLiveBeforeX("var a,b;for(a=0;a<9;a++){X:1}", "a"); assertLiveAfterX("var a,b;for(a in b){X:b};", "a"); // For-In should serve as a gen as well. assertLiveBeforeX("var a,b; X:for(a in b){ }", "a"); // "a in b" should kill "a" before it. // Can't prove this unless we have branched backward DFA. //assertNotLiveAfterX("var a,b;X:b;for(a in b){a()};", "a"); // Unless it is used before. assertLiveBeforeX("var a,b;X:a();b();for(a in b){a()};", "a"); // Initializer assertLiveBeforeX("var a,b;X:b;for(b=a;;){};", "a"); assertNotLiveBeforeX("var a,b;X:a;for(b=a;;){b()};b();", "b"); } public void testNestedLoops() { assertLiveBeforeX("var a;X:while(1){while(1){a()}}", "a"); assertLiveBeforeX("var a;X:while(1){while(1){while(1){a()}}}", "a"); assertLiveBeforeX("var a;X:while(1){while(1){a()};a=1}", "a"); assertLiveAfterX("var a;while(1){while(1){a()};X:a=1;}", "a"); assertLiveAfterX("var a;while(1){X:a=1;while(1){a()}}", "a"); assertNotLiveBeforeX( "var a;X:1;do{do{do{a=1;}while(1)}while(1)}while(1);a()", "a"); } public void testSwitches() { assertLiveBeforeX("var a,b;X:switch(a){}", "a"); assertLiveBeforeX("var a,b;X:switch(b){case(a):break;}", "a"); assertLiveBeforeX("var a,b;X:switch(b){case(b):case(a):break;}", "a"); assertNotLiveBeforeX( "var a,b;X:switch(b){case 1:a=1;break;default:a=2;break};a()", "a"); assertLiveBeforeX("var a,b;X:switch(b){default:a();break;}", "a"); } public void testAssignAndReadInCondition() { // BUG #1358904 // Technically, this isn't exactly true....but we haven't model control flow // within an instruction. assertLiveBeforeX("var a, b; X: if ((a = this) && (b = a)) {}", "a"); assertNotLiveBeforeX("var a, b; X: a = 1, b = 1;", "a"); assertNotLiveBeforeX("var a; X: a = 1, a = 1;", "a"); } public void testParam() { // Unused parameter should not be live. assertNotLiveAfterX("var a;X:a()", "param1"); assertLiveBeforeX("var a;X:a(param1)", "param1"); assertNotLiveAfterX("var a;X:a();a(param2)", "param1"); } public void testExpressionInForIn() { assertLiveBeforeX("var a = [0]; X:for (a[1] in foo) { }", "a"); } public void testArgumentsArray() { // Check that use of arguments forces the parameters into the // escaped set. assertEscaped("arguments[0]", "param1"); assertEscaped("arguments[0]", "param2"); assertEscaped("var args = arguments", "param1"); assertEscaped("var args = arguments", "param2"); assertNotEscaped("arguments = []", "param1"); assertNotEscaped("arguments = []", "param2"); assertEscaped("arguments[0] = 1", "param1"); assertEscaped("arguments[0] = 1", "param2"); assertEscaped("arguments[arguments[0]] = 1", "param1"); assertEscaped("arguments[arguments[0]] = 1", "param2"); } public void testTryCatchFinally() { assertLiveAfterX("var a; try {X:a=1} finally {a}", "a"); assertLiveAfterX("var a; try {a()} catch(e) {X:a=1} finally {a}", "a"); // Because the outer catch doesn't catch any exceptions at all, the read of // "a" within the catch block should not make "a" live. assertNotLiveAfterX("var a = 1; try {" + "try {a()} catch(e) {X:1} } catch(E) {a}", "a"); assertLiveAfterX("var a; while(1) { try {X:a=1;break} finally {a}}", "a"); } public void testForInAssignment() { assertLiveBeforeX("var a,b; for (var y in a = b) { X:a[y] }", "a"); // No one refers to b after the first iteration. assertNotLiveBeforeX("var a,b; for (var y in a = b) { X:a[y] }", "b"); assertLiveBeforeX("var a,b; for (var y in a = b) { X:a[y] }", "y"); assertLiveAfterX("var a,b; for (var y in a = b) { a[y]; X: y();}", "a"); } public void testExceptionThrowingAssignments() { assertLiveBeforeX("try{var a; X:a=foo();a} catch(e) {e()}", "a"); assertLiveBeforeX("try{X:var a=foo();a} catch(e) {e()}", "a"); assertLiveBeforeX("try{X:var a=foo()} catch(e) {e(a)}", "a"); } public void testInnerFunctions() { assertLiveBeforeX("function a() {}; X: a()", "a"); assertNotLiveBeforeX("X: function a() {}", "a"); assertLiveBeforeX("a = function(){}; function a() {}; X: a()", "a"); // NOTE: function a() {} has no CFG node representation since it is not // part of the control execution. assertLiveAfterX("X: a = function(){}; function a() {}; a()", "a"); assertNotLiveBeforeX("X: a = function(){}; function a() {}; a()", "a"); } public void testEscaped() { assertEscaped("var a;function b(){a()}", "a"); assertEscaped("var a;function b(){param1()}", "param1"); assertEscaped("var a;function b(){function c(){a()}}", "a"); assertEscaped("var a;function b(){param1.x = function() {a()}}", "a"); assertEscaped("try{} catch(e){}", "e"); assertNotEscaped("var a;function b(){var c; c()}", "c"); assertNotEscaped("var a;function f(){function b(){var c;c()}}", "c"); assertNotEscaped("var a;function b(){};a()", "a"); assertNotEscaped("var a;function f(){function b(){}}a()", "a"); assertNotEscaped("var a;function b(){var a;a()};a()", "a"); // Escaped by exporting. assertEscaped("var _x", "_x"); } public void testEscapedLiveness() { assertNotLiveBeforeX("var a;X:a();function b(){a()}", "a"); } public void testBug1449316() { assertLiveBeforeX("try {var x=[]; X:var y=x[0]} finally {foo()}", "x"); } private void assertLiveBeforeX(String src, String var) { FlowState state = getFlowStateAtX(src); assertNotNull(src + " should contain a label 'X:'", state); assertTrue("Variable" + var + " should be live before X", state.getIn() .isLive(liveness.getVarIndex(var))); } private void assertLiveAfterX(String src, String var) { FlowState state = getFlowStateAtX(src); assertTrue("Label X should be in the input program.", state != null); assertTrue("Variable" + var + " should be live after X", state.getOut() .isLive(liveness.getVarIndex(var))); } private void assertNotLiveAfterX(String src, String var) { FlowState state = getFlowStateAtX(src); assertTrue("Label X should be in the input program.", state != null); assertTrue("Variable" + var + " should not be live after X", !state .getOut().isLive(liveness.getVarIndex(var))); } private void assertNotLiveBeforeX(String src, String var) { FlowState state = getFlowStateAtX(src); assertTrue("Label X should be in the input program.", state != null); assertTrue("Variable" + var + " should not be live before X", !state .getIn().isLive(liveness.getVarIndex(var))); } private FlowState getFlowStateAtX( String src) { liveness = computeLiveness(src); return getFlowStateAtX(liveness.getCfg().getEntry().getValue(), liveness .getCfg()); } private FlowState getFlowStateAtX( Node node, ControlFlowGraph cfg) { if (node.isLabel()) { if (node.getFirstChild().getString().equals("X")) { return cfg.getNode(node.getLastChild()).getAnnotation(); } } for (Node c = node.getFirstChild(); c != null; c = c.getNext()) { FlowState state = getFlowStateAtX(c, cfg); if (state != null) { return state; } } return null; } private static void assertEscaped(String src, String name) { for (Var var : computeLiveness(src).getEscapedLocals()) { if (var.name.equals(name)) { return; } } fail("Variable " + name + " should be in the escaped local list."); } private static void assertNotEscaped(String src, String name) { for (Var var : computeLiveness(src).getEscapedLocals()) { assertFalse(var.name.equals(name)); } } private static LiveVariablesAnalysis computeLiveness(String src) { Compiler compiler = new Compiler(); CompilerOptions options = new CompilerOptions(); options.setCodingConvention(new GoogleCodingConvention()); compiler.initOptions(options); src = "function _FUNCTION(param1, param2){" + src + "}"; Node n = compiler.parseTestCode(src).removeFirstChild(); Node script = new Node(Token.SCRIPT, n); script.setInputId(new InputId("test")); assertEquals(0, compiler.getErrorCount()); Scope scope = new SyntacticScopeCreator(compiler).createScope( n, Scope.createGlobalScope(script)); ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, true); cfa.process(null, n); ControlFlowGraph cfg = cfa.getCfg(); LiveVariablesAnalysis analysis = new LiveVariablesAnalysis(cfg, scope, compiler); analysis.analyze(); return analysis; } }