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