/* * Copyright 2011 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; /** * @author johnlenz@google.com (John Lenz) */ public final class RemoveUnusedClassPropertiesTest extends CompilerTestCase { private static final String EXTERNS = LINE_JOINER.join( "var window;", "function alert(a) {}", "var EXT = {};", "EXT.ext;", "var Object;", "Object.defineProperties;", "var foo"); public RemoveUnusedClassPropertiesTest() { super(EXTERNS); enableGatherExternProperties(); } @Override protected CompilerPass getProcessor(Compiler compiler) { return new RemoveUnusedClassProperties(compiler, true); } @Override protected void setUp() throws Exception { super.setUp(); setAcceptedLanguage(LanguageMode.ECMASCRIPT5); } public void testSimple1() { // A property defined on "this" can be removed test("this.a = 2", "2"); test("x = (this.a = 2)", "x = 2"); testSame("this.a = 2; x = this.a;"); } public void testSimple2() { // A property defined on "this" can be removed, even when defined // as part of an expression test("this.a = 2, f()", "2, f()"); test("x = (this.a = 2, f())", "x = (2, f())"); test("x = (f(), this.a = 2)", "x = (f(), 2)"); } public void testSimple3() { // A property defined on an object other than "this" can not be removed. testSame("y.a = 2"); // and prevents the removal of the definition on 'this'. testSame("y.a = 2; this.a = 2"); // Some use of the property "a" prevents the removal. testSame("y.a = 2; this.a = 1; alert(x.a)"); } public void testObjLit() { // A property defined on an object other than "this" can not be removed. testSame("({a:2})"); // and prevent the removal of the definition on 'this'. testSame("({a:0}); this.a = 1;"); // Some use of the property "a" prevents the removal. testSame("x = ({a:0}); this.a = 1; alert(x.a)"); } public void testExtern() { // A property defined in the externs is can not be removed. testSame("this.ext = 2"); } public void testExport() { // An exported property can not be removed. testSame("this.ext = 2; window['export'] = this.ext;"); testSame("function f() { this.ext = 2; } window['export'] = this.ext;"); } public void testAssignOp1() { // Properties defined using a compound assignment can be removed if the // result of the assignment expression is not immediately used. test("this.x += 2", "2"); testSame("x = (this.x += 2)"); testSame("this.x += 2; x = this.x;"); // But, of course, a later use prevents its removal. testSame("this.x += 2; x.x;"); } public void testAssignOp2() { // Properties defined using a compound assignment can be removed if the // result of the assignment expression is not immediately used. test("this.a += 2, f()", "2, f()"); test("x = (this.a += 2, f())", "x = (2, f())"); testSame("x = (f(), this.a += 2)"); } public void testAssignOpPrototype() { test("SomeSideEffect().prototype.x = 0", "SomeSideEffect(), 0"); } public void testInc1() { // Increments and Decrements are handled similarly to compound assignments // but need a placeholder value when replaced. test("this.x++", "0"); testSame("x = (this.x++)"); testSame("this.x++; x = this.x;"); test("--this.x", "0"); testSame("x = (--this.x)"); testSame("--this.x; x = this.x;"); } public void testInc2() { // Increments and Decrements are handled similarly to compound assignments // but need a placeholder value when replaced. test("this.a++, f()", "0, f()"); test("x = (this.a++, f())", "x = (0, f())"); testSame("x = (f(), this.a++)"); test("--this.a, f()", "0, f()"); test("x = (--this.a, f())", "x = (0, f())"); testSame("x = (f(), --this.a)"); } public void testIncPrototype() { test("SomeSideEffect().prototype.x++", "SomeSideEffect(), 0"); } public void testExprResult() { test("this.x", "0"); test("c.prototype.x", "0"); test("SomeSideEffect().prototype.x", "SomeSideEffect(),0"); } public void testJSCompiler_renameProperty() { // JSCompiler_renameProperty introduces a use of the property testSame("this.a = 2; x[JSCompiler_renameProperty('a')]"); testSame("this.a = 2; JSCompiler_renameProperty('a')"); } public void testForIn() { // This is the basic assumption that this pass makes: // it can remove properties even when the object is used in a FOR-IN loop test("this.y = 1;for (var a in x) { alert(x[a]) }", "1;for (var a in x) { alert(x[a]) }"); } public void testObjectKeys() { // This is the basic assumption that this pass makes: // it can remove properties even when the object are referenced test("this.y = 1;alert(Object.keys(this))", "1;alert(Object.keys(this))"); } public void testObjectReflection1() { // Verify reflection prevents removal. testSame( "/** @constructor */ function A() {this.foo = 1;}\n" + "use(goog.reflect.object(A, {foo: 'foo'}));\n"); } public void testObjectReflection2() { // Any object literal definition prevents removal. // Type based removal would allow this to be removed. testSame( "/** @constructor */ function A() {this.foo = 1;}\n" + "use({foo: 'foo'});\n"); } public void testIssue730() { // Partial removal of properties can causes problems if the object is // sealed. testSame( "function A() {this.foo = 0;}\n" + "function B() {this.a = new A();}\n" + "B.prototype.dostuff = function() {this.a.foo++;alert('hi');}\n" + "new B().dostuff();\n"); } public void testNoRemoveSideEffect1() { test( "function A() {alert('me'); return function(){}}\n" + "A().prototype.foo = function() {};\n", "function A() {alert('me'); return function(){}}\n" + "A(),function(){};\n"); } public void testNoRemoveSideEffect2() { test( "function A() {alert('me'); return function(){}}\n" + "A().prototype.foo++;\n", "function A() {alert('me'); return function(){}}\n" + "A(),0;\n"); } public void testPrototypeProps1() { test( "function A() {this.foo = 1;}\n" + "A.prototype.foo = 0;\n" + "A.prototype.method = function() {this.foo++};\n" + "new A().method()\n", "function A() {1;}\n" + "0;\n" + "A.prototype.method = function() {0;};\n" + "new A().method()\n"); } public void testPrototypeProps2() { // don't remove properties that are exported by convention testSame( "function A() {this._foo = 1;}\n" + "A.prototype._foo = 0;\n" + "A.prototype.method = function() {this._foo++};\n" + "new A().method()\n"); } public void testConstructorProperty1() { enableTypeCheck(); test( "/** @constructor */ function C() {} C.prop = 1;", "/** @constructor */ function C() {} 1"); } public void testConstructorProperty2() { enableTypeCheck(); testSame( "/** @constructor */ function C() {} " + "C.prop = 1; " + "function use(a) { alert(a.prop) }; " + "use(C)"); } public void testObjectDefineProperties1() { enableTypeCheck(); testSame( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {prop:{value:1}});", "function use(a) { alert(a.prop) };", "use(C)")); } public void testObjectDefineProperties2() { enableTypeCheck(); test( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {prop:{value:1}});"), LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {});")); } public void testObjectDefineProperties3() { enableTypeCheck(); test( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, ", " {prop:{", " get:function(){},", " set:function(a){},", "}});"), LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {});")); } // side-effect in definition retains property public void testObjectDefineProperties4() { enableTypeCheck(); testSame( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {prop:alert('')});")); } // quoted properties retains property public void testObjectDefineProperties5() { enableTypeCheck(); testSame( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {'prop': {value: 1}});")); } public void testObjectDefineProperties6() { enableTypeCheck(); // an unknown destination object doesn't prevent removal. test( "Object.defineProperties(foo(), {prop:{value:1}});", "Object.defineProperties(foo(), {});"); } public void testObjectDefineProperties7() { enableTypeCheck(); test( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {prop:{get:function () {return new C}}});"), LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {});")); } public void testObjectDefineProperties8() { enableTypeCheck(); test( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {prop:{set:function (a) {return alert(a)}}});"), LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {});")); } public void testObjectDefineProperties_used_setter_removed() { // TODO: Either remove, fix this, or document it as a limitation of advanced mode optimizations. enableTypeCheck(); test( LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {prop:{set:function (a) {alert(2)}}});", "C.prop = 2;"), LINE_JOINER.join( "/** @constructor */ function C() {}", "Object.defineProperties(C, {});2")); } public void testEs6GettersWithoutTranspilation() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); test("class C { get value() { return 0; } }", "class C {}"); testSame("class C { get value() { return 0; } } const x = (new C()).value"); } public void testES6ClassComputedProperty() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); testSame("class C { ['test' + 3]() { return 0; } }"); } public void testEs6SettersWithoutTranspilation() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); test("class C { set value(val) { this.internalVal = val; } }", "class C {}"); test( "class C { set value(val) { this.internalVal = val; } } (new C()).value = 3;", "class C { set value(val) { val; } } (new C()).value = 3;"); testSame( LINE_JOINER.join( "class C {", " set value(val) {", " this.internalVal = val;", " }", " get value() {", " return this.internalVal;", " }", "}", "const y = new C();", "y.value = 3;", "const x = y.value;")); } // All object literal fields are not removed, but the following // tests assert that the pass does not fail. public void testEs6EnhancedObjLiteralsComputedValuesNotRemoved() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); testSame( LINE_JOINER.join( "function getCar(make, model, value) {", " return {", " ['make' + make] : true", " };", "}")); } public void testEs6EnhancedObjLiteralsMethodShortHandNotRemoved() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); testSame( LINE_JOINER.join( "function getCar(make, model, value) {", " return {", " getModel() {", " return model;", " }", " };", "}")); } public void testEs6EnhancedObjLiteralsPropertyShorthand() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); testSame("function getCar(make, model, value) { return {model}; }"); } public void testEs6GettersRemoval() { enableTypeCheck(); test( // This is the output of ES6->ES5 class getter converter. // See Es6ToEs3ConverterTest.testEs5GettersAndSettersClasses test method. LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " get: function() {", " return 0;", " }", " }", "});"), LINE_JOINER.join( "/** @constructor @struct */var C=function(){};", "0;", "$jscomp.global.Object.defineProperties(C.prototype, {});")); } public void testEs6SettersRemoval() { enableTypeCheck(); test( // This is the output of ES6->ES5 class setter converter. // See Es6ToEs3ConverterTest.testEs5GettersAndSettersClasses test method. LINE_JOINER.join( "/** @constructor @struct */", "var C = function() {};", "/** @type {?} */", "C.prototype.value;", "/** @type {?} */", "C.prototype.internalVal;", "$jscomp.global.Object.defineProperties(C.prototype, {", " value: {", " configurable: true,", " enumerable: true,", " /** @this {C} */", " set: function(val) {", " this.internalVal = val;", " }", " }", "});"), LINE_JOINER.join( "/** @constructor @struct */var C=function(){};", "0;", "0;", "$jscomp.global.Object.defineProperties(C.prototype, {});")); } }