/* * Copyright 2006 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 RemoveUnusedPrototypeProperties}. * * @author nicksantos@google.com (Nick Santos) */ public final class RemoveUnusedPrototypePropertiesTest extends CompilerTestCase { private static final String EXTERNS = "IFoo.prototype.bar; var mExtern; mExtern.bExtern; mExtern['cExtern'];"; private boolean canRemoveExterns = false; private boolean anchorUnusedVars = false; public RemoveUnusedPrototypePropertiesTest() { super(EXTERNS); } @Override protected CompilerPass getProcessor(Compiler compiler) { return new RemoveUnusedPrototypeProperties(compiler, canRemoveExterns, anchorUnusedVars); } @Override protected void setUp() throws Exception { super.setUp(); setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); anchorUnusedVars = false; canRemoveExterns = false; } public void testAnalyzePrototypeProperties() { // Basic removal for prototype properties test("function e(){}" + "e.prototype.a = function(){};" + "e.prototype.b = function(){};" + "var x = new e; x.a()", "function e(){}" + "e.prototype.a = function(){};" + "var x = new e; x.a()"); // Basic removal for prototype replacement test("function e(){}" + "e.prototype = {a: function(){}, b: function(){}};" + "var x=new e; x.a()", "function e(){}" + "e.prototype = {a: function(){}};" + "var x = new e; x.a()"); // Unused properties that were referenced in the externs file should not be // removed test("function e(){}" + "e.prototype.a = function(){};" + "e.prototype.bExtern = function(){};" + "var x = new e;x.a()", "function e(){}" + "e.prototype.a = function(){};" + "e.prototype.bExtern = function(){};" + "var x = new e; x.a()"); testSame("function e(){}" + "e.prototype = {a: function(){}, bExtern: function(){}};" + "var x = new e; x.a()"); } public void testAliasing1() { // Aliasing a property is not enough for it to count as used test("function e(){}" + "e.prototype.method1 = function(){};" + "e.prototype.method2 = function(){};" + // aliases "e.prototype.alias1 = e.prototype.method1;" + "e.prototype.alias2 = e.prototype.method2;" + "var x = new e; x.method1()", "function e(){}" + "e.prototype.method1 = function(){};" + "var x = new e; x.method1()"); // Using an alias should keep it test("function e(){}" + "e.prototype.method1 = function(){};" + "e.prototype.method2 = function(){};" + // aliases "e.prototype.alias1 = e.prototype.method1;" + "e.prototype.alias2 = e.prototype.method2;" + "var x=new e; x.alias1()", "function e(){}" + "e.prototype.method1 = function(){};" + "e.prototype.alias1 = e.prototype.method1;" + "var x = new e; x.alias1()"); } public void testAliasing2() { // Aliasing a property is not enough for it to count as used test("function e(){}" + "e.prototype.method1 = function(){};" + // aliases "e.prototype.alias1 = e.prototype.method1;" + "(new e).method1()", "function e(){}" + "e.prototype.method1 = function(){};" + "(new e).method1()"); // Using an alias should keep it testSame( "function e(){}" + "e.prototype.method1 = function(){};" // aliases + "e.prototype.alias1 = e.prototype.method1;" + "(new e).alias1()"); } public void testAliasing3() { // Aliasing a property is not enough for it to count as used test("function e(){}" + "e.prototype.method1 = function(){};" + "e.prototype.method2 = function(){};" + // aliases "e.prototype['alias1'] = e.prototype.method1;" + "e.prototype['alias2'] = e.prototype.method2;", "function e(){}" + "e.prototype.method1=function(){};" + "e.prototype.method2=function(){};" + "e.prototype[\"alias1\"]=e.prototype.method1;" + "e.prototype[\"alias2\"]=e.prototype.method2;"); } public void testAliasing4() { // Aliasing a property is not enough for it to count as used test("function e(){}" + "e.prototype['alias1'] = e.prototype.method1 = function(){};" + "e.prototype['alias2'] = e.prototype.method2 = function(){};", "function e(){}" + "e.prototype[\"alias1\"]=e.prototype.method1=function(){};" + "e.prototype[\"alias2\"]=e.prototype.method2=function(){};"); } public void testAliasing5() { // An exported alias must preserved any referenced values in the // referenced function. test("function e(){}" + "e.prototype.method1 = function(){this.method2()};" + "e.prototype.method2 = function(){};" + // aliases "e.prototype['alias1'] = e.prototype.method1;", "function e(){}" + "e.prototype.method1=function(){this.method2()};" + "e.prototype.method2=function(){};" + "e.prototype[\"alias1\"]=e.prototype.method1;"); } public void testAliasing6() { // An exported alias must preserved any referenced values in the // referenced function. test("function e(){}" + "e.prototype.method1 = function(){this.method2()};" + "e.prototype.method2 = function(){};" + // aliases "window['alias1'] = e.prototype.method1;", "function e(){}" + "e.prototype.method1=function(){this.method2()};" + "e.prototype.method2=function(){};" + "window['alias1']=e.prototype.method1;"); } public void testAliasing7() { // An exported alias must preserved any referenced values in the // referenced function. testSame("function e(){}" + "e.prototype['alias1'] = e.prototype.method1 = " + "function(){this.method2()};" + "e.prototype.method2 = function(){};"); } public void testStatementRestriction() { testSame("function e(){}" + "var x = e.prototype.method1 = function(){};" + "var y = new e; x()"); } public void testExportedMethodsByNamingConvention() { String classAndItsMethodAliasedAsExtern = "function Foo() {}" + "Foo.prototype.method = function() {};" + // not removed "Foo.prototype.unused = function() {};" + // removed "var _externInstance = new Foo();" + "Foo.prototype._externMethod = Foo.prototype.method"; // aliased here String compiled = "function Foo(){}" + "Foo.prototype.method = function(){};" + "var _externInstance = new Foo;" + "Foo.prototype._externMethod = Foo.prototype.method"; test(classAndItsMethodAliasedAsExtern, compiled); } public void testMethodsFromExternsFileNotExported() { canRemoveExterns = true; String classAndItsMethodAliasedAsExtern = "function Foo() {}" + "Foo.prototype.bar_ = function() {};" + "Foo.prototype.unused = function() {};" + "var instance = new Foo;" + "Foo.prototype.bar = Foo.prototype.bar_"; String compiled = "function Foo(){}" + "var instance = new Foo;"; test(classAndItsMethodAliasedAsExtern, compiled); } public void testExportedMethodsByNamingConventionAlwaysExported() { canRemoveExterns = true; String classAndItsMethodAliasedAsExtern = "function Foo() {}" + "Foo.prototype.method = function() {};" + // not removed "Foo.prototype.unused = function() {};" + // removed "var _externInstance = new Foo();" + "Foo.prototype._externMethod = Foo.prototype.method"; // aliased here String compiled = "function Foo(){}" + "Foo.prototype.method = function(){};" + "var _externInstance = new Foo;" + "Foo.prototype._externMethod = Foo.prototype.method"; test(classAndItsMethodAliasedAsExtern, compiled); } public void testExternMethodsFromExternsFile() { String classAndItsMethodAliasedAsExtern = "function Foo() {}" + "Foo.prototype.bar_ = function() {};" + // not removed "Foo.prototype.unused = function() {};" + // removed "var instance = new Foo;" + "Foo.prototype.bar = Foo.prototype.bar_"; // aliased here String compiled = "function Foo(){}" + "Foo.prototype.bar_ = function(){};" + "var instance = new Foo;" + "Foo.prototype.bar = Foo.prototype.bar_"; test(classAndItsMethodAliasedAsExtern, compiled); } public void testPropertyReferenceGraph() { // test a prototype property graph that looks like so: // b -> a, c -> b, c -> a, d -> c, e -> a, e -> f String constructor = "function Foo() {}"; String defA = "Foo.prototype.a = function() { Foo.superClass_.a.call(this); };"; String defB = "Foo.prototype.b = function() { this.a(); };"; String defC = "Foo.prototype.c = function() { " + "Foo.superClass_.c.call(this); this.b(); this.a(); };"; String defD = "Foo.prototype.d = function() { this.c(); };"; String defE = "Foo.prototype.e = function() { this.a(); this.f(); };"; String defF = "Foo.prototype.f = function() { };"; String fullClassDef = constructor + defA + defB + defC + defD + defE + defF; // ensure that all prototypes are compiled out if none are used test(fullClassDef, ""); // make sure that the right prototypes are called for each use String callA = "(new Foo()).a();"; String callB = "(new Foo()).b();"; String callC = "(new Foo()).c();"; String callD = "(new Foo()).d();"; String callE = "(new Foo()).e();"; String callF = "(new Foo()).f();"; test(fullClassDef + callA, constructor + defA + callA); test(fullClassDef + callB, constructor + defA + defB + callB); test(fullClassDef + callC, constructor + defA + defB + defC + callC); test(fullClassDef + callD, constructor + defA + defB + defC + defD + callD); test(fullClassDef + callE, constructor + defA + defE + defF + callE); test(fullClassDef + callF, constructor + defF + callF); test(fullClassDef + callA + callC, constructor + defA + defB + defC + callA + callC); test(fullClassDef + callB + callC, constructor + defA + defB + defC + callB + callC); test(fullClassDef + callA + callB + callC, constructor + defA + defB + defC + callA + callB + callC); } public void testPropertiesDefinedWithGetElem() { testSame("function Foo() {} Foo.prototype['elem'] = function() {};"); testSame("function Foo() {} Foo.prototype[1 + 1] = function() {};"); } public void testQuotedProperties() { // Basic removal for prototype replacement testSame("function e(){}" + "e.prototype = {'a': function(){}, 'b': function(){}};"); } public void testNeverRemoveImplicitlyUsedProperties() { testSame("function Foo() {} " + "Foo.prototype.length = 3; " + "Foo.prototype.toString = function() { return 'Foo'; }; " + "Foo.prototype.valueOf = function() { return 'Foo'; }; "); } public void testPropertyDefinedInBranch() { test("function Foo() {} if (true) Foo.prototype.baz = function() {};", "if (true);"); test("function Foo() {} while (true) Foo.prototype.baz = function() {};", "while (true);"); test("function Foo() {} for (;;) Foo.prototype.baz = function() {};", "for (;;);"); test("function Foo() {} do Foo.prototype.baz = function() {}; while(true);", "do; while(true);"); } public void testUsingAnonymousObjectsToDefeatRemoval() { String constructor = "function Foo() {}"; String declaration = constructor + "Foo.prototype.baz = 3;"; test(declaration, ""); testSame(declaration + "var x = {}; x.baz = 5;"); testSame(declaration + "var x = {baz: 5};"); test(declaration + "var x = {'baz': 5};", "var x = {'baz': 5};"); } public void testGlobalFunctionsInGraph() { test( "var x = function() { (new Foo).baz(); };" + "var y = function() { x(); };" + "function Foo() {}" + "Foo.prototype.baz = function() { y(); };", ""); } public void testGlobalFunctionsInGraph2() { // In this example, Foo.prototype.baz is a global reference to // Foo, and Foo has a reference to baz. So everything stays in. // TODO(nicksantos): We should be able to make the graph more fine-grained // here. Instead of Foo.prototype.bar creating a global reference to Foo, // it should create a reference from .bar to Foo. That will let us // compile this away to nothing. testSame( "var x = function() { (new Foo).baz(); };" + "var y = function() { x(); };" + "function Foo() { this.baz(); }" + "Foo.prototype.baz = function() { y(); };"); } public void testGlobalFunctionsInGraph3() { test( "var x = function() { (new Foo).baz(); };" + "var y = function() { x(); };" + "function Foo() { this.baz(); }" + "Foo.prototype.baz = function() { x(); };", "var x = function() { (new Foo).baz(); };" + "function Foo() { this.baz(); }" + "Foo.prototype.baz = function() { x(); };"); } public void testGlobalFunctionsInGraph4() { test( "var x = function() { (new Foo).baz(); };" + "var y = function() { x(); };" + "function Foo() { Foo.prototype.baz = function() { y(); }; }", ""); } public void testGlobalFunctionsInGraph5() { test( "function Foo() {}" + "Foo.prototype.methodA = function() {};" + "function x() { (new Foo).methodA(); }" + "Foo.prototype.methodB = function() { x(); };", ""); anchorUnusedVars = true; test( "function Foo() {}" + "Foo.prototype.methodA = function() {};" + "function x() { (new Foo).methodA(); }" + "Foo.prototype.methodB = function() { x(); };", "function Foo() {}" + "Foo.prototype.methodA = function() {};" + "function x() { (new Foo).methodA(); }"); } public void testGlobalFunctionsInGraph6() { testSame( "function Foo() {}" + "Foo.prototype.methodA = function() {};" + "function x() { (new Foo).methodA(); }" + "Foo.prototype.methodB = function() { x(); };" + "(new Foo).methodB();"); } public void testGlobalFunctionsInGraph7() { testSame( "function Foo() {}" + "Foo.prototype.methodA = function() {};" + "this.methodA();"); } public void testGetterBaseline() { anchorUnusedVars = true; test( "function Foo() {}" + "Foo.prototype = { " + " methodA: function() {}," + " methodB: function() { x(); }" + "};" + "function x() { (new Foo).methodA(); }", "function Foo() {}" + "Foo.prototype = { " + " methodA: function() {}" + "};" + "function x() { (new Foo).methodA(); }"); } public void testGetter1() { test( "function Foo() {}" + "Foo.prototype = { " + " get methodA() {}," + " get methodB() { x(); }" + "};" + "function x() { (new Foo).methodA; }", "function Foo() {}" + "Foo.prototype = {};"); anchorUnusedVars = true; test( "function Foo() {}" + "Foo.prototype = { " + " get methodA() {}," + " get methodB() { x(); }" + "};" + "function x() { (new Foo).methodA; }", "function Foo() {}" + "Foo.prototype = { " + " get methodA() {}" + "};" + "function x() { (new Foo).methodA; }"); } public void testGetter2() { anchorUnusedVars = true; test( "function Foo() {}" + "Foo.prototype = { " + " get methodA() {}," + " set methodA(a) {}," + " get methodB() { x(); }," + " set methodB(a) { x(); }" + "};" + "function x() { (new Foo).methodA; }", "function Foo() {}" + "Foo.prototype = { " + " get methodA() {}," + " set methodA(a) {}" + "};" + "function x() { (new Foo).methodA; }"); } public void testHook1() throws Exception { test( "/** @constructor */ function Foo() {}" + "Foo.prototype.method1 = Math.random() ?" + " function() { this.method2(); } : function() { this.method3(); };" + "Foo.prototype.method2 = function() {};" + "Foo.prototype.method3 = function() {};", ""); } public void testHook2() throws Exception { testSame( "/** @constructor */ function Foo() {}" + "Foo.prototype.method1 = Math.random() ?" + " function() { this.method2(); } : function() { this.method3(); };" + "Foo.prototype.method2 = function() {};" + "Foo.prototype.method3 = function() {};" + "(new Foo()).method1();"); } public void testRemoveInBlock() { test(LINE_JOINER.join( "if (true) {", " if (true) {", " var foo = function() {};", " }", "}"), LINE_JOINER.join( "if (true) {", " if (true) {", " }", "}")); testSame("if (true) { let foo = function() {} }"); } }