/*
* 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 static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Joiner;
import com.google.javascript.rhino.FunctionTypeI;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TypeI;
/**
* Tests for {@link DevirtualizePrototypeMethods}
*
*/
public final class DevirtualizePrototypeMethodsTest extends TypeICompilerTestCase {
private static final String EXTERNAL_SYMBOLS =
DEFAULT_EXTERNS + "var extern;extern.externalMethod";
public DevirtualizePrototypeMethodsTest() {
super(EXTERNAL_SYMBOLS);
}
@Override
protected int getNumRepetitions() {
// run pass once.
return 1;
}
@Override
protected void setUp() throws Exception {
super.setUp();
this.mode = TypeInferenceMode.NEITHER;
}
/**
* Combine source strings using ';' as the separator.
*/
private static String semicolonJoin(String ... parts) {
return Joiner.on(";").join(parts);
}
public void testRewritePrototypeMethodsWithCorrectTypes() throws Exception {
String input =
LINE_JOINER.join(
"/** @constructor */",
"function A() { this.x = 3; }",
"/** @return {number} */",
"A.prototype.foo = function() { return this.x; };",
"/** @param {number} p",
" @return {number} */",
"A.prototype.bar = function(p) { return this.x; };",
"A.prototype.baz = function() {};",
"var o = new A();",
"o.foo();",
"o.bar(2);",
"o.baz()");
String expected =
LINE_JOINER.join(
"/** @constructor */",
"function A(){ this.x = 3; }",
"var JSCompiler_StaticMethods_foo = ",
"function(JSCompiler_StaticMethods_foo$self) {",
" return JSCompiler_StaticMethods_foo$self.x",
"};",
"var JSCompiler_StaticMethods_bar = ",
"function(JSCompiler_StaticMethods_bar$self, p) {",
" return JSCompiler_StaticMethods_bar$self.x",
"};",
"var JSCompiler_StaticMethods_baz = ",
"function(JSCompiler_StaticMethods_baz$self) {",
"};",
"var o = new A();",
"JSCompiler_StaticMethods_foo(o);",
"JSCompiler_StaticMethods_bar(o, 2);",
"JSCompiler_StaticMethods_baz(o)");
this.mode = TypeInferenceMode.OTI_ONLY;
test(input, expected);
checkTypeOfRewrittenMethods();
this.mode = TypeInferenceMode.NTI_ONLY;
test(input, expected);
checkTypeOfRewrittenMethods();
}
private void checkTypeOfRewrittenMethods() {
TypeI thisType = getTypeAtPosition(0).toMaybeFunctionType().getInstanceType();
FunctionTypeI fooType = getTypeAtPosition(1, 0, 0).toMaybeFunctionType();
FunctionTypeI barType = getTypeAtPosition(2, 0, 0).toMaybeFunctionType();
FunctionTypeI bazType = getTypeAtPosition(3, 0, 0).toMaybeFunctionType();
TypeI fooResultType = getTypeAtPosition(5, 0);
TypeI barResultType = getTypeAtPosition(6, 0);
TypeI bazResultType = getTypeAtPosition(7, 0);
TypeI number = fooResultType;
TypeI receiver = fooType.getTypeOfThis();
assertTrue("Expected number: " + number, number.isNumberValueType());
// NOTE: OTI has the receiver as unknown, NTI has it as null.
assertTrue(
"Expected null or unknown: " + receiver, receiver == null || receiver.isUnknownType());
assertThat(barResultType).isEqualTo(number);
// Check that foo's type is {function(A): number}
assertThat(fooType.getParameterTypes()).containsExactly(thisType);
assertThat(fooType.getReturnType()).isEqualTo(number);
assertThat(fooType.getTypeOfThis()).isEqualTo(receiver);
// Check that bar's type is {function(A, number): number}
assertThat(barType.getParameterTypes()).containsExactly(thisType, number).inOrder();
assertThat(barType.getReturnType()).isEqualTo(number);
assertThat(barType.getTypeOfThis()).isEqualTo(receiver);
// Check that baz's type is {function(A): undefined} in OTI and {function(A): ?} in NTI
assertThat(bazType.getParameterTypes()).containsExactly(thisType);
assertThat(bazType.getTypeOfThis()).isEqualTo(receiver);
// TODO(sdh): NTI currently fails to infer the result of the baz() call (b/37351897)
// so we handle it more carefully. When methods are deferred, this should be changed
// to check that it's exactly unknown.
assertTrue(
"Expected undefined or unknown: " + bazResultType,
bazResultType.isVoidType() || bazResultType.isUnknownType());
assertTrue(
"Expected undefined: " + bazType.getReturnType(), bazType.getReturnType().isVoidType());
}
private TypeI getTypeAtPosition(int... indices) {
Node node = getLastCompiler().getJsRoot().getFirstChild();
for (int index : indices) {
node = node.getChildAtIndex(index);
}
return node.getTypeI();
}
public void testRewriteChained() throws Exception {
String source =
LINE_JOINER.join(
"A.prototype.foo = function(){return this.b};",
"B.prototype.bar = function(){};",
"o.foo().bar()");
String expected =
LINE_JOINER.join(
"var JSCompiler_StaticMethods_foo = ",
"function(JSCompiler_StaticMethods_foo$self) {",
" return JSCompiler_StaticMethods_foo$self.b",
"};",
"var JSCompiler_StaticMethods_bar = ",
"function(JSCompiler_StaticMethods_bar$self) {",
"};",
"JSCompiler_StaticMethods_bar(JSCompiler_StaticMethods_foo(o))");
test(source, expected);
}
/**
* Inputs for declaration used as an r-value tests.
*/
private static class NoRewriteDeclarationUsedAsRValue {
static final String DECL = "a.prototype.foo = function() {}";
static final String CALL = "o.foo()";
private NoRewriteDeclarationUsedAsRValue() {}
}
public void testRewriteDeclIsExpressionStatement() throws Exception {
test(semicolonJoin(NoRewriteDeclarationUsedAsRValue.DECL,
NoRewriteDeclarationUsedAsRValue.CALL),
"var JSCompiler_StaticMethods_foo =" +
"function(JSCompiler_StaticMethods_foo$self) {};" +
"JSCompiler_StaticMethods_foo(o)");
}
public void testNoRewriteDeclUsedAsAssignmentRhs() throws Exception {
testSame(semicolonJoin("var c = " + NoRewriteDeclarationUsedAsRValue.DECL,
NoRewriteDeclarationUsedAsRValue.CALL));
}
public void testNoRewriteDeclUsedAsCallArgument() throws Exception {
testSame(semicolonJoin("f(" + NoRewriteDeclarationUsedAsRValue.DECL + ")",
NoRewriteDeclarationUsedAsRValue.CALL));
}
/**
* Inputs for restrict-to-global-scope tests.
*/
private static class NoRewriteIfNotInGlobalScopeTestInput {
static final String INPUT =
LINE_JOINER.join(
"function a(){}",
"a.prototype.foo = function() {return this.x};",
"var o = new a;",
"o.foo()");
private NoRewriteIfNotInGlobalScopeTestInput() {}
}
public void testRewriteInGlobalScope() throws Exception {
String expected =
LINE_JOINER.join(
"function a(){}",
"var JSCompiler_StaticMethods_foo = ",
"function(JSCompiler_StaticMethods_foo$self) {",
" return JSCompiler_StaticMethods_foo$self.x",
"};",
"var o = new a;",
"JSCompiler_StaticMethods_foo(o);");
test(NoRewriteIfNotInGlobalScopeTestInput.INPUT, expected);
}
public void testNoRewriteIfNotInGlobalScope1() throws Exception {
setAcceptedLanguage(CompilerOptions.LanguageMode.ECMASCRIPT_2015);
testSame("if(true){" + NoRewriteIfNotInGlobalScopeTestInput.INPUT + "}");
}
public void testNoRewriteIfNotInGlobalScope2() throws Exception {
testSame("function enclosingFunction() {" +
NoRewriteIfNotInGlobalScopeTestInput.INPUT +
"}");
}
public void testNoRewriteNamespaceFunctions() throws Exception {
String source = "function a(){}; a.foo = function() {return this.x}; a.foo()";
testSame(source);
}
public void testRewriteIfDuplicates() throws Exception {
test(
LINE_JOINER.join(
"function A(){}; A.prototype.getFoo = function() { return 1; }; ",
"function B(){}; B.prototype.getFoo = function() { return 1; }; ",
"var x = Math.random() ? new A() : new B();",
"alert(x.getFoo());"),
LINE_JOINER.join(
"function A(){}; ",
"var JSCompiler_StaticMethods_getFoo=",
"function(JSCompiler_StaticMethods_getFoo$self){return 1};",
"function B(){};",
"B.prototype.getFoo=function(){return 1};",
"var x = Math.random() ? new A() : new B();",
"alert(JSCompiler_StaticMethods_getFoo(x));"));
}
public void testRewriteIfDuplicatesWithThis() throws Exception {
test(
LINE_JOINER.join(
"function A(){}; A.prototype.getFoo = ",
"function() { return this._foo + 1; }; ",
"function B(){}; B.prototype.getFoo = ",
"function() { return this._foo + 1; }; ",
"var x = Math.random() ? new A() : new B();",
"alert(x.getFoo());"),
LINE_JOINER.join(
"function A(){}; ",
"var JSCompiler_StaticMethods_getFoo=",
"function(JSCompiler_StaticMethods_getFoo$self){",
" return JSCompiler_StaticMethods_getFoo$self._foo + 1",
"};",
"function B(){};",
"B.prototype.getFoo=function(){return this._foo + 1};",
"var x = Math.random() ? new A() : new B();",
"alert(JSCompiler_StaticMethods_getFoo(x));"));
}
public void testNoRewriteIfDuplicates() throws Exception {
testSame(
LINE_JOINER.join(
"function A(){}; A.prototype.getFoo = function() { return 1; }; ",
"function B(){}; B.prototype.getFoo = function() { return 2; }; ",
"var x = Math.random() ? new A() : new B();",
"alert(x.getFoo());"));
}
/**
* Inputs for object literal tests.
*/
private static class NoRewritePrototypeObjectLiteralsTestInput {
static final String REGULAR = "b.prototype.foo = function() { return 1; }";
static final String OBJ_LIT = "a.prototype = {foo : function() { return 2; }}";
static final String CALL = "o.foo()";
private NoRewritePrototypeObjectLiteralsTestInput() {}
}
public void testRewritePrototypeNoObjectLiterals() throws Exception {
test(
semicolonJoin(
NoRewritePrototypeObjectLiteralsTestInput.REGULAR,
NoRewritePrototypeObjectLiteralsTestInput.CALL),
LINE_JOINER.join(
"var JSCompiler_StaticMethods_foo = ",
"function(JSCompiler_StaticMethods_foo$self) { return 1; };",
"JSCompiler_StaticMethods_foo(o)"));
}
public void testRewritePrototypeObjectLiterals1() throws Exception {
test(
semicolonJoin(
NoRewritePrototypeObjectLiteralsTestInput.OBJ_LIT,
NoRewritePrototypeObjectLiteralsTestInput.CALL),
LINE_JOINER.join(
"a.prototype={};",
"var JSCompiler_StaticMethods_foo=",
"function(JSCompiler_StaticMethods_foo$self){ return 2; };",
"JSCompiler_StaticMethods_foo(o)"));
}
public void testNoRewritePrototypeObjectLiterals2() throws Exception {
testSame(semicolonJoin(NoRewritePrototypeObjectLiteralsTestInput.OBJ_LIT,
NoRewritePrototypeObjectLiteralsTestInput.REGULAR,
NoRewritePrototypeObjectLiteralsTestInput.CALL));
}
public void testNoRewriteExternalMethods1() throws Exception {
testSame("a.externalMethod()");
}
public void testNoRewriteExternalMethods2() throws Exception {
testSame("A.prototype.externalMethod = function(){}; o.externalMethod()");
}
public void testNoRewriteCodingConvention() throws Exception {
// no rename, leading _ indicates exported symbol
testSame("a.prototype._foo = function() {};");
}
public void testRewriteNoVarArgs() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype.foo = function(args) {return args};",
"var o = new a;",
"o.foo()");
String expected =
LINE_JOINER.join(
"function a(){}",
"var JSCompiler_StaticMethods_foo = ",
" function(JSCompiler_StaticMethods_foo$self, args) {return args};",
"var o = new a;",
"JSCompiler_StaticMethods_foo(o)");
test(source, expected);
}
public void testNoRewriteVarArgs() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype.foo = function(var_args) {return arguments};",
"var o = new a;",
"o.foo()");
testSame(source);
}
/**
* Inputs for invalidating reference tests.
*/
private static class NoRewriteNonCallReferenceTestInput {
static final String BASE =
"function a(){}\na.prototype.foo = function() {return this.x};\nvar o = new a;";
private NoRewriteNonCallReferenceTestInput() {}
}
public void testRewriteCallReference() throws Exception {
String expected =
LINE_JOINER.join(
"function a(){}",
"var JSCompiler_StaticMethods_foo = ",
"function(JSCompiler_StaticMethods_foo$self) {",
" return JSCompiler_StaticMethods_foo$self.x",
"};",
"var o = new a;",
"JSCompiler_StaticMethods_foo(o);");
test(NoRewriteNonCallReferenceTestInput.BASE + "o.foo()", expected);
}
public void testNoRewriteNoReferences() throws Exception {
testSame(NoRewriteNonCallReferenceTestInput.BASE);
}
public void testNoRewriteNonCallReference() throws Exception {
testSame(NoRewriteNonCallReferenceTestInput.BASE + "o.foo && o.foo()");
}
/**
* Inputs for nested definition tests.
*/
private static class NoRewriteNestedFunctionTestInput {
static final String PREFIX = "a.prototype.foo = function() {";
static final String SUFFIX = "o.foo()";
static final String INNER = "a.prototype.bar = function() {}; o.bar()";
static final String EXPECTED_PREFIX =
"var JSCompiler_StaticMethods_foo=" +
"function(JSCompiler_StaticMethods_foo$self){";
static final String EXPECTED_SUFFIX =
"JSCompiler_StaticMethods_foo(o)";
private NoRewriteNestedFunctionTestInput() {}
}
public void testRewriteNoNestedFunction() throws Exception {
test(semicolonJoin(
NoRewriteNestedFunctionTestInput.PREFIX + "}",
NoRewriteNestedFunctionTestInput.SUFFIX,
NoRewriteNestedFunctionTestInput.INNER),
semicolonJoin(
NoRewriteNestedFunctionTestInput.EXPECTED_PREFIX + "}",
NoRewriteNestedFunctionTestInput.EXPECTED_SUFFIX,
"var JSCompiler_StaticMethods_bar=" +
"function(JSCompiler_StaticMethods_bar$self){}",
"JSCompiler_StaticMethods_bar(o)"));
}
public void testNoRewriteNestedFunction() throws Exception {
test(NoRewriteNestedFunctionTestInput.PREFIX +
NoRewriteNestedFunctionTestInput.INNER + "};" +
NoRewriteNestedFunctionTestInput.SUFFIX,
NoRewriteNestedFunctionTestInput.EXPECTED_PREFIX +
NoRewriteNestedFunctionTestInput.INNER + "};" +
NoRewriteNestedFunctionTestInput.EXPECTED_SUFFIX);
}
public void testRewriteImplementedMethod() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype.foo = function(args) {return args};",
"var o = new a;",
"o.foo()");
String expected =
LINE_JOINER.join(
"function a(){}",
"var JSCompiler_StaticMethods_foo = ",
" function(JSCompiler_StaticMethods_foo$self, args) {return args};",
"var o = new a;",
"JSCompiler_StaticMethods_foo(o)");
test(source, expected);
}
public void testRewriteImplementedMethod2() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype['foo'] = function(args) {return args};",
"var o = new a;",
"o.foo()");
testSame(source);
}
public void testRewriteImplementedMethod3() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype.foo = function(args) {return args};",
"var o = new a;",
"o['foo']");
testSame(source);
}
public void testRewriteImplementedMethod4() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype['foo'] = function(args) {return args};",
"var o = new a;",
"o['foo']");
testSame(source);
}
public void testRewriteImplementedMethod5() throws Exception {
String source = "(function() {this.foo()}).prototype.foo = function() {extern();};";
testSame(source);
}
public void testRewriteImplementedMethodInObj() throws Exception {
String source =
LINE_JOINER.join(
"function a(){}",
"a.prototype = {foo: function(args) {return args}};",
"var o = new a;",
"o.foo()");
test(source,
"function a(){}" +
"a.prototype={};" +
"var JSCompiler_StaticMethods_foo=" +
"function(JSCompiler_StaticMethods_foo$self,args){return args};" +
"var o=new a;" +
"JSCompiler_StaticMethods_foo(o)");
}
public void testNoRewriteGet1() throws Exception {
// Getters and setter require special handling.
testSame("function a(){}; a.prototype = {get foo(){return f}}; var o = new a; o.foo()");
}
public void testNoRewriteGet2() throws Exception {
// Getters and setter require special handling.
testSame("function a(){}; a.prototype = {get foo(){return 1}}; var o = new a; o.foo");
}
public void testNoRewriteSet1() throws Exception {
// Getters and setter require special handling.
String source = "function a(){}; a.prototype = {set foo(a){}}; var o = new a; o.foo()";
testSame(source);
}
public void testNoRewriteSet2() throws Exception {
// Getters and setter require special handling.
String source = "function a(){}; a.prototype = {set foo(a){}}; var o = new a; o.foo = 1";
testSame(source);
}
public void testNoRewriteNotImplementedMethod() throws Exception {
testSame("function a(){}; var o = new a; o.foo()");
}
public void testWrapper() {
testSame("(function() {})()");
}
private static class ModuleTestInput {
static final String DEFINITION = "a.prototype.foo = function() {}";
static final String USE = "x.foo()";
static final String REWRITTEN_DEFINITION =
"var JSCompiler_StaticMethods_foo=" +
"function(JSCompiler_StaticMethods_foo$self){}";
static final String REWRITTEN_USE =
"JSCompiler_StaticMethods_foo(x)";
private ModuleTestInput() {}
}
public void testRewriteSameModule1() throws Exception {
JSModule[] modules = createModuleStar(
// m1
semicolonJoin(ModuleTestInput.DEFINITION,
ModuleTestInput.USE),
// m2
"");
test(modules, new String[] {
// m1
semicolonJoin(ModuleTestInput.REWRITTEN_DEFINITION,
ModuleTestInput.REWRITTEN_USE),
// m2
"",
});
}
public void testRewriteSameModule2() throws Exception {
JSModule[] modules = createModuleStar(
// m1
"",
// m2
semicolonJoin(ModuleTestInput.DEFINITION,
ModuleTestInput.USE));
test(modules, new String[] {
// m1
"",
// m2
semicolonJoin(ModuleTestInput.REWRITTEN_DEFINITION,
ModuleTestInput.REWRITTEN_USE)
});
}
public void testRewriteSameModule3() throws Exception {
JSModule[] modules = createModuleStar(
// m1
semicolonJoin(ModuleTestInput.USE,
ModuleTestInput.DEFINITION),
// m2
"");
test(modules, new String[] {
// m1
semicolonJoin(ModuleTestInput.REWRITTEN_USE,
ModuleTestInput.REWRITTEN_DEFINITION),
// m2
""
});
}
public void testRewriteDefinitionBeforeUse() throws Exception {
JSModule[] modules = createModuleStar(
// m1
ModuleTestInput.DEFINITION,
// m2
ModuleTestInput.USE);
test(modules, new String[] {
// m1
ModuleTestInput.REWRITTEN_DEFINITION,
// m2
ModuleTestInput.REWRITTEN_USE
});
}
public void testNoRewriteUseBeforeDefinition() throws Exception {
JSModule[] modules = createModuleStar(
// m1
ModuleTestInput.USE,
// m2
ModuleTestInput.DEFINITION);
testSame(modules);
}
@Override
protected CompilerPass getProcessor(Compiler compiler) {
return new DevirtualizePrototypeMethods(compiler);
}
}