/* * Copyright 2004 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.CompilerOptions.LanguageMode; /** * Tests for {@link PeepholeSubstituteAlternateSyntax} in isolation. * Tests for the interaction of multiple peephole passes are in * PeepholeIntegrationTest. */ public final class PeepholeSubstituteAlternateSyntaxTest extends CompilerTestCase { // Externs for built-in constructors // Needed for testFoldLiteralObjectConstructors(), // testFoldLiteralArrayConstructors() and testFoldRegExp...() private static final String FOLD_CONSTANTS_TEST_EXTERNS = "var window = {};\n" + "var Object = function f(){};\n" + "var RegExp = function f(a){};\n" + "var Array = function f(a){};\n" + "window.foo = null;\n"; private boolean late = true; public PeepholeSubstituteAlternateSyntaxTest() { super(FOLD_CONSTANTS_TEST_EXTERNS); } @Override public void setUp() throws Exception { super.setUp(); late = true; disableNormalize(); } @Override protected CompilerPass getProcessor(final Compiler compiler) { PeepholeOptimizationsPass peepholePass = new PeepholeOptimizationsPass( compiler, new PeepholeSubstituteAlternateSyntax(late)); peepholePass.setRetraverseOnChange(false); return peepholePass; } @Override protected int getNumRepetitions() { return 1; } private void foldSame(String js) { testSame(js); } private void fold(String js, String expected) { test(js, expected); } public void testFoldRegExpConstructor() { enableNormalize(); // Cannot fold all the way to a literal because there are too few arguments. fold("x = new RegExp", "x = RegExp()"); // Empty regexp should not fold to // since that is a line comment in JS fold("x = new RegExp(\"\")", "x = RegExp(\"\")"); fold("x = new RegExp(\"\", \"i\")", "x = RegExp(\"\",\"i\")"); // Bogus flags should not fold testSame("x = RegExp(\"foobar\", \"bogus\")", PeepholeSubstituteAlternateSyntax.INVALID_REGULAR_EXPRESSION_FLAGS); // Don't fold if the argument is not a string. See issue 1260. foldSame("x = new RegExp(y)"); // Can Fold fold("x = new RegExp(\"foobar\")", "x = /foobar/"); fold("x = RegExp(\"foobar\")", "x = /foobar/"); fold("x = new RegExp(\"foobar\", \"i\")", "x = /foobar/i"); // Make sure that escaping works fold("x = new RegExp(\"\\\\.\", \"i\")", "x = /\\./i"); fold("x = new RegExp(\"/\", \"\")", "x = /\\//"); fold("x = new RegExp(\"[/]\", \"\")", "x = /[/]/"); fold("x = new RegExp(\"///\", \"\")", "x = /\\/\\/\\//"); fold("x = new RegExp(\"\\\\\\/\", \"\")", "x = /\\//"); fold("x = new RegExp(\"\\n\")", "x = /\\n/"); fold("x = new RegExp('\\\\\\r')", "x = /\\r/"); // Shouldn't fold RegExp unnormalized because // we can't be sure that RegExp hasn't been redefined disableNormalize(); foldSame("x = new RegExp(\"foobar\")"); } public void testVersionSpecificRegExpQuirks() { enableNormalize(); // Don't fold if the flags contain 'g' setAcceptedLanguage(LanguageMode.ECMASCRIPT3); fold("x = new RegExp(\"foobar\", \"g\")", "x = RegExp(\"foobar\",\"g\")"); fold("x = new RegExp(\"foobar\", \"ig\")", "x = RegExp(\"foobar\",\"ig\")"); // ... unless in ECMAScript 5 mode per section 7.8.5 of ECMAScript 5. setAcceptedLanguage(LanguageMode.ECMASCRIPT5); fold("x = new RegExp(\"foobar\", \"ig\")", "x = /foobar/ig"); // Don't fold things that crash older versions of Safari and that don't work // as regex literals on other old versions of Safari setAcceptedLanguage(LanguageMode.ECMASCRIPT3); fold("x = new RegExp(\"\\u2028\")", "x = RegExp(\"\\u2028\")"); fold("x = new RegExp(\"\\\\\\\\u2028\")", "x = /\\\\u2028/"); // Sunset Safari exclusions for ECMAScript 5 and later. setAcceptedLanguage(LanguageMode.ECMASCRIPT5); fold("x = new RegExp(\"\\u2028\\u2029\")", "x = /\\u2028\\u2029/"); fold("x = new RegExp(\"\\\\u2028\")", "x = /\\u2028/"); fold("x = new RegExp(\"\\\\\\\\u2028\")", "x = /\\\\u2028/"); } public void testFoldRegExpConstructorStringCompare() { enableNormalize(); test("x = new RegExp(\"\\n\", \"i\")", "x = /\\n/i"); } public void testContainsUnicodeEscape() throws Exception { assertFalse(PeepholeSubstituteAlternateSyntax.containsUnicodeEscape("")); assertFalse(PeepholeSubstituteAlternateSyntax.containsUnicodeEscape("foo")); assertTrue(PeepholeSubstituteAlternateSyntax.containsUnicodeEscape( "\u2028")); assertTrue(PeepholeSubstituteAlternateSyntax.containsUnicodeEscape( "\\u2028")); assertTrue( PeepholeSubstituteAlternateSyntax.containsUnicodeEscape("foo\\u2028")); assertFalse(PeepholeSubstituteAlternateSyntax.containsUnicodeEscape( "foo\\\\u2028")); assertTrue(PeepholeSubstituteAlternateSyntax.containsUnicodeEscape( "foo\\\\u2028bar\\u2028")); } public void testFoldLiteralObjectConstructors() { enableNormalize(); // Can fold when normalized fold("x = new Object", "x = ({})"); fold("x = new Object()", "x = ({})"); fold("x = Object()", "x = ({})"); disableNormalize(); // Cannot fold above when not normalized foldSame("x = new Object"); foldSame("x = new Object()"); foldSame("x = Object()"); enableNormalize(); // Cannot fold, the constructor being used is actually a local function foldSame("x = " + "(function f(){function Object(){this.x=4};return new Object();})();"); } public void testFoldLiteralObjectConstructors_onWindow() { enableNormalize(); // Can fold when normalized fold("x = new window.Object", "x = ({})"); fold("x = new window.Object()", "x = ({})"); fold("x = window.Object()", "x = ({})"); disableNormalize(); // Cannot fold above when not normalized foldSame("x = new window.Object"); foldSame("x = new window.Object()"); foldSame("x = window.Object()"); enableNormalize(); // Can fold, the window namespace ensures it's not a conflict with the local Object. fold("x = (function f(){function Object(){this.x=4};return new window.Object;})();", "x = (function f(){function Object(){this.x=4};return {};})();"); } public void testFoldLiteralArrayConstructors() { enableNormalize(); // No arguments - can fold when normalized fold("x = new Array", "x = []"); fold("x = new Array()", "x = []"); fold("x = Array()", "x = []"); // One argument - can be fold when normalized fold("x = new Array(0)", "x = []"); fold("x = Array(0)", "x = []"); fold("x = new Array(\"a\")", "x = [\"a\"]"); fold("x = Array(\"a\")", "x = [\"a\"]"); // One argument - cannot be fold when normalized fold("x = new Array(7)", "x = Array(7)"); foldSame("x = Array(7)"); fold("x = new Array(y)", "x = Array(y)"); foldSame("x = Array(y)"); fold("x = new Array(foo())", "x = Array(foo())"); foldSame("x = Array(foo())"); // More than one argument - can be fold when normalized fold("x = new Array(1, 2, 3, 4)", "x = [1, 2, 3, 4]"); fold("x = Array(1, 2, 3, 4)", "x = [1, 2, 3, 4]"); fold("x = new Array('a', 1, 2, 'bc', 3, {}, 'abc')", "x = ['a', 1, 2, 'bc', 3, {}, 'abc']"); fold("x = Array('a', 1, 2, 'bc', 3, {}, 'abc')", "x = ['a', 1, 2, 'bc', 3, {}, 'abc']"); fold("x = new Array(Array(1, '2', 3, '4'))", "x = [[1, '2', 3, '4']]"); fold("x = Array(Array(1, '2', 3, '4'))", "x = [[1, '2', 3, '4']]"); fold("x = new Array(Object(), Array(\"abc\", Object(), Array(Array())))", "x = [{}, [\"abc\", {}, [[]]]]"); fold("x = new Array(Object(), Array(\"abc\", Object(), Array(Array())))", "x = [{}, [\"abc\", {}, [[]]]]"); disableNormalize(); // Cannot fold above when not normalized foldSame("x = new Array"); foldSame("x = new Array()"); foldSame("x = Array()"); foldSame("x = new Array(0)"); foldSame("x = Array(0)"); foldSame("x = new Array(\"a\")"); foldSame("x = Array(\"a\")"); foldSame("x = new Array(7)"); foldSame("x = Array(7)"); foldSame("x = new Array(foo())"); foldSame("x = Array(foo())"); foldSame("x = new Array(1, 2, 3, 4)"); foldSame("x = Array(1, 2, 3, 4)"); foldSame("x = new Array('a', 1, 2, 'bc', 3, {}, 'abc')"); foldSame("x = Array('a', 1, 2, 'bc', 3, {}, 'abc')"); foldSame("x = new Array(Array(1, '2', 3, '4'))"); foldSame("x = Array(Array(1, '2', 3, '4'))"); foldSame("x = new Array(" + "Object(), Array(\"abc\", Object(), Array(Array())))"); foldSame("x = new Array(" + "Object(), Array(\"abc\", Object(), Array(Array())))"); } public void testRemoveWindowRefs() { enableNormalize(); fold("x = window.Object", "x = Object"); fold("x = window.Object.keys", "x = Object.keys"); fold("if (window.Object) {}", "if (Object) {}"); fold("x = window.Object", "x = Object"); fold("x = window.Array", "x = Array"); fold("x = window.Error", "x = Error"); fold("x = window.RegExp", "x = RegExp"); fold("x = window.Math", "x = Math"); // Not currently handled by the pass but should be folded in the future. foldSame("x = window.String"); // Don't fold properties on the window. foldSame("x = window.foo"); disableNormalize(); foldSame("x = window.Object"); foldSame("x = window.Object.keys"); enableNormalize(); foldSame("var x = " + "(function f(){var window = {Object: function() {}};return new window.Object;})();"); } public void testFoldStandardConstructors() { foldSame("new Foo('a')"); foldSame("var x = new goog.Foo(1)"); foldSame("var x = new String(1)"); foldSame("var x = new Number(1)"); foldSame("var x = new Boolean(1)"); enableNormalize(); fold("var x = new Object('a')", "var x = Object('a')"); fold("var x = new RegExp('')", "var x = RegExp('')"); fold("var x = new Error('20')", "var x = Error(\"20\")"); fold("var x = new Array(20)", "var x = Array(20)"); } public void testFoldTrueFalse() { fold("x = true", "x = !0"); fold("x = false", "x = !1"); } public void testFoldTrueFalseComparison() { fold("x == true", "x == 1"); fold("x == false", "x == 0"); fold("x != true", "x != 1"); fold("x < true", "x < 1"); fold("x <= true", "x <= 1"); fold("x > true", "x > 1"); fold("x >= true", "x >= 1"); } public void testFoldSubtractionAssignment() { fold("x -= 1", "--x"); fold("x -= -1", "++x"); } public void testFoldReturnResult() { foldSame("function f(){return !1;}"); foldSame("function f(){return null;}"); fold("function f(){return void 0;}", "function f(){return}"); foldSame("function f(){return void foo();}"); fold("function f(){return undefined;}", "function f(){return}"); fold("function f(){if(a()){return undefined;}}", "function f(){if(a()){return}}"); } public void testUndefined() { foldSame("var x = undefined"); foldSame("function f(f) {var undefined=2;var x = undefined;}"); enableNormalize(); fold("var x = undefined", "var x=void 0"); foldSame( "var undefined = 1;" + "function f() {var undefined=2;var x = undefined;}"); foldSame("function f(undefined) {}"); foldSame("try {} catch(undefined) {}"); foldSame("for (undefined in {}) {}"); foldSame("undefined++;"); disableNormalize(); foldSame("undefined += undefined;"); enableNormalize(); fold("undefined += undefined;", "undefined = void 0 + void 0;"); } public void testSplitCommaExpressions() { late = false; // Don't try to split in expressions. foldSame("while (foo(), !0) boo()"); foldSame("var a = (foo(), !0);"); foldSame("a = (foo(), !0);"); // Don't try to split COMMA under LABELs. foldSame("a:a(),b()"); fold("(x=2), foo()", "x=2; foo()"); fold("foo(), boo();", "foo(); boo()"); fold("(a(), b()), (c(), d());", "a(); b(); (c(), d());"); fold("a(); b(); (c(), d());", "a(); b(); c(); d();"); fold("foo(), true", "foo();true"); foldSame("foo();true"); fold("function x(){foo(), !0}", "function x(){foo(); !0}"); foldSame("function x(){foo(); !0}"); } public void testComma1() { late = false; fold("1, 2", "1; 2"); late = true; foldSame("1, 2"); } public void testComma2() { late = false; test("1, a()", "1; a()"); late = true; foldSame("1, a()"); } public void testComma3() { late = false; test("1, a(), b()", "1; a(); b()"); late = true; foldSame("1, a(), b()"); } public void testComma4() { late = false; test("a(), b()", "a();b()"); late = true; foldSame("a(), b()"); } public void testComma5() { late = false; test("a(), b(), 1", "a();b();1"); late = true; foldSame("a(), b(), 1"); } public void testStringArraySplitting() { testSame("var x=['1','2','3','4']"); testSame("var x=['1','2','3','4','5']"); test("var x=['1','2','3','4','5','6']", "var x='123456'.split('')"); test("var x=['1','2','3','4','5','00']", "var x='1 2 3 4 5 00'.split(' ')"); test("var x=['1','2','3','4','5','6','7']", "var x='1234567'.split('')"); test("var x=['1','2','3','4','5','6','00']", "var x='1 2 3 4 5 6 00'.split(' ')"); test("var x=[' ,',',',',',',',',',',']", "var x=' ,;,;,;,;,;,'.split(';')"); test("var x=[',,',' ',',',',',',',',']", "var x=',,; ;,;,;,;,'.split(';')"); test("var x=['a,',' ',',',',',',',',']", "var x='a,; ;,;,;,;,'.split(';')"); // all possible delimiters used, leave it alone testSame("var x=[',', ' ', ';', '{', '}']"); } public void testBindToCall1() { test("(goog.bind(f))()", "f()"); test("(goog.bind(f,a))()", "f.call(a)"); test("(goog.bind(f,a,b))()", "f.call(a,b)"); test("(goog.bind(f))(a)", "f(a)"); test("(goog.bind(f,a))(b)", "f.call(a,b)"); test("(goog.bind(f,a,b))(c)", "f.call(a,b,c)"); test("(goog.partial(f))()", "f()"); test("(goog.partial(f,a))()", "f(a)"); test("(goog.partial(f,a,b))()", "f(a,b)"); test("(goog.partial(f))(a)", "f(a)"); test("(goog.partial(f,a))(b)", "f(a,b)"); test("(goog.partial(f,a,b))(c)", "f(a,b,c)"); test("((function(){}).bind())()", "((function(){}))()"); test("((function(){}).bind(a))()", "((function(){})).call(a)"); test("((function(){}).bind(a,b))()", "((function(){})).call(a,b)"); test("((function(){}).bind())(a)", "((function(){}))(a)"); test("((function(){}).bind(a))(b)", "((function(){})).call(a,b)"); test("((function(){}).bind(a,b))(c)", "((function(){})).call(a,b,c)"); // Without using type information we don't know "f" is a function. testSame("(f.bind())()"); testSame("(f.bind(a))()"); testSame("(f.bind())(a)"); testSame("(f.bind(a))(b)"); // Don't rewrite if the bind isn't the immediate call target testSame("(goog.bind(f)).call(g)"); } public void testBindToCall2() { test("(goog$bind(f))()", "f()"); test("(goog$bind(f,a))()", "f.call(a)"); test("(goog$bind(f,a,b))()", "f.call(a,b)"); test("(goog$bind(f))(a)", "f(a)"); test("(goog$bind(f,a))(b)", "f.call(a,b)"); test("(goog$bind(f,a,b))(c)", "f.call(a,b,c)"); test("(goog$partial(f))()", "f()"); test("(goog$partial(f,a))()", "f(a)"); test("(goog$partial(f,a,b))()", "f(a,b)"); test("(goog$partial(f))(a)", "f(a)"); test("(goog$partial(f,a))(b)", "f(a,b)"); test("(goog$partial(f,a,b))(c)", "f(a,b,c)"); // Don't rewrite if the bind isn't the immediate call target testSame("(goog$bind(f)).call(g)"); } public void testBindToCall3() { // TODO(johnlenz): The code generator wraps free calls with (0,...) to // prevent leaking "this", but the parser doesn't unfold it, making a // AST comparison fail. For now do a string comparison to validate the // correct code is in fact generated. // The FREE call wrapping should be moved out of the code generator // and into a denormalizing pass. new StringCompareTestCase().testBindToCall3(); } public void testSimpleFunctionCall1() { test("var a = String(23)", "var a = '' + 23"); test("var a = String('hello')", "var a = '' + 'hello'"); testSame("var a = String('hello', bar());"); testSame("var a = String({valueOf: function() { return 1; }});"); } public void testSimpleFunctionCall2() { test("var a = Boolean(true)", "var a = !0"); test("var a = Boolean(false)", "var a = !1"); test("var a = Boolean(1)", "var a = !!1"); test("var a = Boolean(x)", "var a = !!x"); test("var a = Boolean({})", "var a = !!{}"); testSame("var a = Boolean()"); testSame("var a = Boolean(!0, !1);"); } public void testRotateAssociativeOperators() { test("a || (b || c); a * (b * c); a | (b | c)", "(a || b) || c; (a * b) * c; (a | b) | c"); testSame("a % (b % c); a / (b / c); a - (b - c);"); test("a * (b % c);", "b % c * a"); test("a * b * (c / d)", "c / d * b * a"); test("(a + b) * (c % d)", "c % d * (a + b)"); testSame("(a / b) * (c % d)"); testSame("(c = 5) * (c % d)"); test("(a + b) * c * (d % e)", "d % e * c * (a + b)"); test("!a * c * (d % e)", "d % e * c * !a"); } public void testNoRotateInfiniteLoop() { test("1/x * (y/1 * (1/z))", "1/x * (y/1) * (1/z)"); testSame("1/x * (y/1) * (1/z)"); } private static class StringCompareTestCase extends CompilerTestCase { StringCompareTestCase() { super("", false); } @Override protected CompilerPass getProcessor(Compiler compiler) { CompilerPass peepholePass = new PeepholeOptimizationsPass(compiler, new PeepholeSubstituteAlternateSyntax(false)); return peepholePass; } public void testBindToCall3() { test("(goog.bind(f.m))()", "(0,f.m)()"); test("(goog.bind(f.m,a))()", "f.m.call(a)"); test("(goog.bind(f.m))(a)", "(0,f.m)(a)"); test("(goog.bind(f.m,a))(b)", "f.m.call(a,b)"); test("(goog.partial(f.m))()", "(0,f.m)()"); test("(goog.partial(f.m,a))()", "(0,f.m)(a)"); test("(goog.partial(f.m))(a)", "(0,f.m)(a)"); test("(goog.partial(f.m,a))(b)", "(0,f.m)(a,b)"); // Without using type information we don't know "f" is a function. testSame("f.m.bind()()"); testSame("f.m.bind(a)()"); testSame("f.m.bind()(a)"); testSame("f.m.bind(a)(b)"); // Don't rewrite if the bind isn't the immediate call target testSame("goog.bind(f.m).call(g)"); } } }