/* * Copyright 2009 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.common.collect.Sets; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; import com.google.javascript.jscomp.ExpressionDecomposer.DecompositionType; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import junit.framework.TestCase; import java.util.Set; import javax.annotation.Nullable; /** * Unit tests for ExpressionDecomposer * @author johnlenz@google.com (John Lenz) */ public class ExpressionDecomposerTest extends TestCase { // Note: functions "foo" and "goo" are external functions // in the helper. public void testCanExposeExpression1() { // Can't move or decompose some classes of expressions. helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "while(foo());", "foo"); helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "while(x = goo()&&foo()){}", "foo"); helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "while(x += goo()&&foo()){}", "foo"); helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "do{}while(foo());", "foo"); helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "for(;foo(););", "foo"); // This case could be supported for loops without conditional continues // by moving the increment into the loop body. helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "for(;;foo());", "foo"); // FOR initializer could be supported but they never occur // as they are normalized away. // This is potentially doable but a bit too complex currently. helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "switch(1){case foo():;}", "foo"); } public void testCanExposeExpression2() { helperCanExposeExpression( DecompositionType.MOVABLE, "foo()", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "x = foo()", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "var x = foo()", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "if(foo()){}", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "switch(foo()){}", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "switch(foo()){}", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "function f(){ return foo();}", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "x = foo() && 1", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "x = foo() || 1", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "x = foo() ? 0 : 1", "foo"); helperCanExposeExpression( DecompositionType.MOVABLE, "(function(a){b = a})(foo())", "foo"); } public void testCanExposeExpression3() { helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "x = 0 && foo()", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "x = 1 || foo()", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "var x = 1 ? foo() : 0", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "goo() && foo()", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "x = goo() && foo()", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "x += goo() && foo()", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "var x = goo() && foo()", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "if(goo() && foo()){}", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "switch(goo() && foo()){}", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "switch(goo() && foo()){}", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "switch(x = goo() && foo()){}", "foo"); helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "function f(){ return goo() && foo();}", "foo"); } public void testCanExposeExpression4() { // 'this' must be preserved in call. helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "if (goo.a(1, foo()));", "foo"); } public void testCanExposeExpression5() { // 'this' must be preserved in call. helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "if (goo['a'](foo()));", "foo"); } public void testCanExposeExpression6() { // 'this' must be preserved in call. helperCanExposeExpression( DecompositionType.UNDECOMPOSABLE, "z:if (goo.a(1, foo()));", "foo"); } public void testCanExposeExpression7() { // Verify calls to function expressions are movable. helperCanExposeFunctionExpression( DecompositionType.MOVABLE, "(function(map){descriptions_=map})(\n" + "function(){\n" + "var ret={};\n" + "ret[INIT]='a';\n" + "ret[MIGRATION_BANNER_DISMISS]='b';\n" + "return ret\n" + "}()\n" + ");", 2); } public void testCanExposeExpression8() { // Can it be decompose? helperCanExposeExpression( DecompositionType.DECOMPOSABLE, "HangoutStarter.prototype.launchHangout = function() {\n" + " var self = a.b;\n" + " var myUrl = new goog.Uri(getDomServices_(self).getDomHelper()." + "getWindow().location.href);\n" + "};", "getDomServices_"); // Verify it is properly expose the target expression. helperExposeExpression( "HangoutStarter.prototype.launchHangout = function() {\n" + " var self = a.b;\n" + " var myUrl = new goog.Uri(getDomServices_(self).getDomHelper()." + "getWindow().location.href);\n" + "};", "getDomServices_", "HangoutStarter.prototype.launchHangout = function() {" + " var self = a.b;" + " var temp_const$$0 = goog.Uri;" + " var myUrl = new temp_const$$0(getDomServices_(self)." + " getDomHelper().getWindow().location.href)}"); // Verify the results can be properly moved. helperMoveExpression( "HangoutStarter.prototype.launchHangout = function() {" + " var self = a.b;" + " var temp_const$$0 = goog.Uri;" + " var myUrl = new temp_const$$0(getDomServices_(self)." + " getDomHelper().getWindow().location.href)}", "getDomServices_", "HangoutStarter.prototype.launchHangout = function() {" + " var self=a.b;" + " var temp_const$$0=goog.Uri;" + " var result$$0=getDomServices_(self);" + " var myUrl=new temp_const$$0(result$$0.getDomHelper()." + " getWindow().location.href)}"); } public void testMoveExpression1() { // There isn't a reason to do this, but it works. helperMoveExpression("foo()", "foo", "var result$$0 = foo(); result$$0;"); } public void testMoveExpression2() { helperMoveExpression( "x = foo()", "foo", "var result$$0 = foo(); x = result$$0;"); } public void testMoveExpression3() { helperMoveExpression( "var x = foo()", "foo", "var result$$0 = foo(); var x = result$$0;"); } public void testMoveExpression4() { helperMoveExpression( "if(foo()){}", "foo", "var result$$0 = foo(); if (result$$0);"); } public void testMoveExpression5() { helperMoveExpression( "switch(foo()){}", "foo", "var result$$0 = foo(); switch(result$$0){}"); } public void testMoveExpression6() { helperMoveExpression( "switch(1 + foo()){}", "foo", "var result$$0 = foo(); switch(1 + result$$0){}"); } public void testMoveExpression7() { helperMoveExpression( "function f(){ return foo();}", "foo", "function f(){ var result$$0 = foo(); return result$$0;}"); } public void testMoveExpression8() { helperMoveExpression( "x = foo() && 1", "foo", "var result$$0 = foo(); x = result$$0 && 1"); } public void testMoveExpression9() { helperMoveExpression( "x = foo() || 1", "foo", "var result$$0 = foo(); x = result$$0 || 1"); } public void testMoveExpression10() { helperMoveExpression( "x = foo() ? 0 : 1", "foo", "var result$$0 = foo(); x = result$$0 ? 0 : 1"); } /* Decomposition tests. */ public void testExposeExpression1() { helperExposeExpression( "x = 0 && foo()", "foo", "var temp$$0; if (temp$$0 = 0) temp$$0 = foo(); x = temp$$0;"); } public void testExposeExpression2() { helperExposeExpression( "x = 1 || foo()", "foo", "var temp$$0; if (temp$$0 = 1); else temp$$0 = foo(); x = temp$$0;"); } public void testExposeExpression3() { helperExposeExpression( "var x = 1 ? foo() : 0", "foo", "var temp$$0;" + " if (1) temp$$0 = foo(); else temp$$0 = 0;var x = temp$$0;"); } public void testExposeExpression4() { helperExposeExpression( "goo() && foo()", "foo", "if (goo()) foo();"); } public void testExposeExpression5() { helperExposeExpression( "x = goo() && foo()", "foo", "var temp$$0; if (temp$$0 = goo()) temp$$0 = foo(); x = temp$$0;"); } public void testExposeExpression6() { helperExposeExpression( "var x = 1 + (goo() && foo())", "foo", "var temp$$0; if (temp$$0 = goo()) temp$$0 = foo();" + "var x = 1 + temp$$0;"); } public void testExposeExpression7() { helperExposeExpression( "if(goo() && foo());", "foo", "var temp$$0;" + "if (temp$$0 = goo()) temp$$0 = foo();" + "if(temp$$0);"); } public void testExposeExpression8() { helperExposeExpression( "switch(goo() && foo()){}", "foo", "var temp$$0;" + "if (temp$$0 = goo()) temp$$0 = foo();" + "switch(temp$$0){}"); } public void testExposeExpression9() { helperExposeExpression( "switch(1 + goo() + foo()){}", "foo", "var temp_const$$0 = 1 + goo();" + "switch(temp_const$$0 + foo()){}"); } public void testExposeExpression10() { helperExposeExpression( "function f(){ return goo() && foo();}", "foo", "function f(){" + "var temp$$0; if (temp$$0 = goo()) temp$$0 = foo();" + "return temp$$0;" + "}"); } public void testExposeExpression11() { // TODO(johnlenz): We really want a constant marking pass. // The value "goo" should be constant, but it isn't known to be so. helperExposeExpression( "if (goo(1, goo(2), (1 ? foo() : 0)));", "foo", "var temp_const$$1 = goo;" + "var temp_const$$0 = goo(2);" + "var temp$$2;" + "if (1) temp$$2 = foo(); else temp$$2 = 0;" + "if (temp_const$$1(1, temp_const$$0, temp$$2));"); } // Simple name on LHS of assignment-op. public void testExposePlusEquals1() { helperExposeExpression( "var x = 0; x += foo() + 1", "foo", "var x = 0; var temp_const$$0 = x;" + "x = temp_const$$0 + (foo() + 1);"); helperExposeExpression( "var x = 0; y = (x += foo()) + x", "foo", "var x = 0; var temp_const$$0 = x;" + "y = (x = temp_const$$0 + foo()) + x"); } // Structure on LHS of assignment-op. public void testExposePlusEquals2() { helperExposeExpression( "var x = {}; x.a += foo() + 1", "foo", "var x = {}; var temp_const$$0 = x;" + "var temp_const$$1 = temp_const$$0.a;" + "temp_const$$0.a = temp_const$$1 + (foo() + 1);"); helperExposeExpression( "var x = {}; y = (x.a += foo()) + x.a", "foo", "var x = {}; var temp_const$$0 = x;" + "var temp_const$$1 = temp_const$$0.a;" + "y = (temp_const$$0.a = temp_const$$1 + foo()) + x.a"); } // Constant object on LHS of assignment-op. public void testExposePlusEquals3() { helperExposeExpression( "/** @const */ var XX = {};\n" + "XX.a += foo() + 1", "foo", "var XX = {}; var temp_const$$0 = XX.a;" + "XX.a = temp_const$$0 + (foo() + 1);"); helperExposeExpression( "var XX = {}; y = (XX.a += foo()) + XX.a", "foo", "var XX = {}; var temp_const$$0 = XX.a;" + "y = (XX.a = temp_const$$0 + foo()) + XX.a"); } // Function all on LHS of assignment-op. public void testExposePlusEquals4() { helperExposeExpression( "var x = {}; goo().a += foo() + 1", "foo", "var x = {};" + "var temp_const$$0 = goo();" + "var temp_const$$1 = temp_const$$0.a;" + "temp_const$$0.a = temp_const$$1 + (foo() + 1);"); helperExposeExpression( "var x = {}; y = (goo().a += foo()) + goo().a", "foo", "var x = {};" + "var temp_const$$0 = goo();" + "var temp_const$$1 = temp_const$$0.a;" + "y = (temp_const$$0.a = temp_const$$1 + foo()) + goo().a"); } // Test multiple levels public void testExposePlusEquals5() { helperExposeExpression( "var x = {}; goo().a.b += foo() + 1", "foo", "var x = {};" + "var temp_const$$0 = goo().a;" + "var temp_const$$1 = temp_const$$0.b;" + "temp_const$$0.b = temp_const$$1 + (foo() + 1);"); helperExposeExpression( "var x = {}; y = (goo().a.b += foo()) + goo().a", "foo", "var x = {};" + "var temp_const$$0 = goo().a;" + "var temp_const$$1 = temp_const$$0.b;" + "y = (temp_const$$0.b = temp_const$$1 + foo()) + goo().a"); } public void testExposeObjectLit1() { // Validate that getter and setters methods are see as side-effect // free and that values can move past them. We don't need to be // concerned with exposing the getter or setter here but the // decomposer does not have a method of exposing properties only variables. helperMoveExpression( "var x = {get a() {}, b: foo()};", "foo", "var result$$0=foo();var x = {get a() {}, b: result$$0};"); helperMoveExpression( "var x = {set a(p) {}, b: foo()};", "foo", "var result$$0=foo();var x = {set a(p) {}, b: result$$0};"); } /** Test case helpers. */ private void helperCanExposeExpression( DecompositionType expectedResult, String code, String fnName ) { helperCanExposeExpression(expectedResult, code, fnName, null); } private void helperCanExposeFunctionExpression( DecompositionType expectedResult, String code, int call) { Compiler compiler = getCompiler(); Set knownConstants = Sets.newHashSet(); ExpressionDecomposer decomposer = new ExpressionDecomposer( compiler, compiler.getUniqueNameIdSupplier(), knownConstants); Node tree = parse(compiler, code); assertNotNull(tree); Node externsRoot = parse(compiler, "function goo() {}" + "function foo() {}"); assertNotNull(externsRoot); Node mainRoot = tree; Node callSite = findCall(tree, null, 2); assertNotNull("Call " + call + " was not found.", callSite); compiler.resetUniqueNameId(); DecompositionType result = decomposer.canExposeExpression( callSite); assertEquals(expectedResult, result); } private void helperCanExposeExpression( DecompositionType expectedResult, String code, String fnName, Set knownConstants ) { Compiler compiler = getCompiler(); if (knownConstants == null) { knownConstants = Sets.newHashSet(); } ExpressionDecomposer decomposer = new ExpressionDecomposer( compiler, compiler.getUniqueNameIdSupplier(), knownConstants); Node tree = parse(compiler, code); assertNotNull(tree); Node externsRoot = parse(compiler, "function goo() {}" + "function foo() {}"); assertNotNull(externsRoot); Node mainRoot = tree; Node callSite = findCall(tree, fnName); assertNotNull("Call to " + fnName + " was not found.", callSite); compiler.resetUniqueNameId(); DecompositionType result = decomposer.canExposeExpression( callSite); assertEquals(expectedResult, result); } private void helperExposeExpression( String code, String fnName, String expectedResult ) { helperExposeExpression( code, fnName, expectedResult, null); } private void validateSourceInfo(Compiler compiler, Node subtree) { (new LineNumberCheck(compiler)).setCheckSubTree(subtree); // Source information problems are reported as compiler errors. if (compiler.getErrorCount() != 0) { String msg = "Error encountered: "; for (JSError err : compiler.getErrors()) { msg += err.toString() + "\n"; } assertTrue(msg, compiler.getErrorCount() == 0); } } private void helperExposeExpression( String code, String fnName, String expectedResult, Set knownConstants ) { Compiler compiler = getCompiler(); if (knownConstants == null) { knownConstants = Sets.newHashSet(); } ExpressionDecomposer decomposer = new ExpressionDecomposer( compiler, compiler.getUniqueNameIdSupplier(), knownConstants); decomposer.setTempNamePrefix("temp"); decomposer.setResultNamePrefix("result"); Node expectedRoot = parse(compiler, expectedResult); Node tree = parse(compiler, code); assertNotNull(tree); Node externsRoot = new Node(Token.EMPTY); Node mainRoot = tree; Node callSite = findCall(tree, fnName); assertNotNull("Call to " + fnName + " was not found.", callSite); DecompositionType result = decomposer.canExposeExpression(callSite); assertTrue(result == DecompositionType.DECOMPOSABLE); compiler.resetUniqueNameId(); decomposer.exposeExpression(callSite); validateSourceInfo(compiler, tree); String explanation = expectedRoot.checkTreeEquals(tree); assertNull("\nExpected: " + compiler.toSource(expectedRoot) + "\nResult: " + compiler.toSource(tree) + "\n" + explanation, explanation); } private void helperMoveExpression( String code, String fnName, String expectedResult ) { helperMoveExpression( code, fnName, expectedResult, null); } private void helperMoveExpression( String code, String fnName, String expectedResult, Set knownConstants ) { Compiler compiler = getCompiler(); if (knownConstants == null) { knownConstants = Sets.newHashSet(); } ExpressionDecomposer decomposer = new ExpressionDecomposer( compiler, compiler.getUniqueNameIdSupplier(), knownConstants); decomposer.setTempNamePrefix("temp"); decomposer.setResultNamePrefix("result"); Node expectedRoot = parse(compiler, expectedResult); Node tree = parse(compiler, code); assertNotNull(tree); Node externsRoot = new Node(Token.EMPTY); Node mainRoot = tree; Node callSite = findCall(tree, fnName); assertNotNull("Call to " + fnName + " was not found.", callSite); compiler.resetUniqueNameId(); decomposer.moveExpression(callSite); validateSourceInfo(compiler, tree); String explanation = expectedRoot.checkTreeEquals(tree); assertNull("\nExpected: " + compiler.toSource(expectedRoot) + "\nResult: " + compiler.toSource(tree) + "\n" + explanation, explanation); } private static Compiler getCompiler() { Compiler compiler = new Compiler(); CompilerOptions options = new CompilerOptions(); options.setLanguageIn(LanguageMode.ECMASCRIPT5); options.setCodingConvention(new GoogleCodingConvention()); compiler.initOptions(options); return compiler; } private static Node findCall(Node n, String name) { return findCall(n, name, 1); } /** * @param name The name to look for. * @param call The call to look for. * @return The return the Nth CALL node to name found in a pre-order * traversal. */ private static Node findCall( Node root, @Nullable final String name, final int call) { class Find { int found = 0; Node find(Node n) { if (n.isCall()) { Node callee = n.getFirstChild(); if (name == null || (callee.isName() && callee.getString().equals(name))) { found++; if (found == call) { return n; } } } for (Node c : n.children()) { Node result = find(c); if (result != null) { return result; } } return null; } } return (new Find()).find(root); } private static Node parse(Compiler compiler, String js) { Node n = Normalize.parseAndNormalizeTestCode(compiler, js, ""); assertEquals(0, compiler.getErrorCount()); return n; } }