/* * Copyright 2016 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.javascript.jscomp.DeadPropertyAssignmentElimination.ASSUME_CONSTRUCTORS_HAVENT_ESCAPED; import com.google.javascript.jscomp.CompilerOptions.LanguageMode; public class DeadPropertyAssignmentEliminationTest extends CompilerTestCase { @Override public void setUp() throws Exception { enableGatherExternProperties(); } public void testBasic() { testSame(LINE_JOINER.join( "var foo = function() {", " this.a = 20;", "}")); test( LINE_JOINER.join( "var foo = function() {", " this.a = 10;", " this.a = 20;", "}"), LINE_JOINER.join( "var foo = function() {", " 10;", " this.a = 20;", "}")); testSame( LINE_JOINER.join( "var foo = function() {", " this.a = 20;", " this.a = this.a + 20;", "}")); } public void testMultipleProperties() { test( LINE_JOINER.join( "var foo = function() {", " this.a = 10;", " this.b = 15;", " this.a = 20;", "}"), LINE_JOINER.join( "var foo = function() {", " 10;", " this.b = 15;", " this.a = 20;", "}")); } public void testNonStandardAssign() { test( LINE_JOINER.join( "var foo = function() {", " this.a = 10;", " this.a += 15;", " this.a = 20;", "}"), LINE_JOINER.join( "var foo = function() {", " 10;", " 15;", " this.a = 20;", "}")); } public void testChainingPropertiesAssignments() { test( LINE_JOINER.join( "var foo = function() {", " this.a = this.b = this.c = 10;", " this.b = 15;", "}"), LINE_JOINER.join( "var foo = function() {", " this.a = this.c = 10;", " this.b = 15;", "}")); } public void testConditionalProperties() { // We don't handle conditionals at all. testSame( LINE_JOINER.join( "var foo = function() {", " this.a = 10;", " if (true) { this.a = 20; } else { this.a = 30; }", "}")); // However, we do handle everything up until the conditional. test( LINE_JOINER.join( "var foo = function() {", " this.a = 10;", " this.a = 20;", " if (true) { this.a = 20; } else { this.a = 30; }", "}"), LINE_JOINER.join( "var foo = function() {", " 10;", " this.a = 20;", " if (true) { this.a = 20; } else { this.a = 30; }", "}")); } public void testQualifiedNamePrefixAssignment() { testSame( LINE_JOINER.join( "var foo = function() {", " a.b.c = 20;", " a.b = other;", " a.b.c = 30;", "}")); testSame( LINE_JOINER.join( "var foo = function() {", " a.b = 20;", " a = other;", " a.b = 30;", "}")); } public void testCall() { testSame( LINE_JOINER.join( "var foo = function() {", " a.b.c = 20;", " doSomething();", " a.b.c = 30;", "}")); if (ASSUME_CONSTRUCTORS_HAVENT_ESCAPED) { test( LINE_JOINER.join( "/** @constructor */", "var foo = function() {", " this.c = 20;", " doSomething();", " this.c = 30;", "}"), LINE_JOINER.join( "/** @constructor */", "var foo = function() {", " 20;", " doSomething();", " this.c = 30;", "}")); } testSame( LINE_JOINER.join( "/** @constructor */", "var foo = function() {", " this.c = 20;", " doSomething(this);", " this.c = 30;", "}")); testSame( LINE_JOINER.join( "/** @constructor */", "var foo = function() {", " this.c = 20;", " this.doSomething();", " this.c = 30;", "}")); testSame( LINE_JOINER.join( "/** @constructor */", "var foo = function() {", " this.c = 20;", " doSomething(this.c);", " this.c = 30;", "}")); } public void testUnknownLookup() { testSame( LINE_JOINER.join( "/** @constructor */", "var foo = function(str) {", " this.x = 5;", " var y = this[str];", " this.x = 10;", "}")); testSame( LINE_JOINER.join( "/** @constructor */", "var foo = function(x, str) {", " x.y = 5;", " var y = x[str];", " x.y = 10;", "}")); } public void testName() { testSame( LINE_JOINER.join( "function f(x) {", " var y = { a: 0 };", " x.a = 123;", " y = x;", " x.a = 234;", " return x.a + y.a;", "}")); } public void testName2() { testSame( LINE_JOINER.join( "function f(x) {", " var y = x;", " x.a = 123;", " x = {};", " x.a = 234;", " return x.a + y.a;", "}")); } public void testAliasing() { testSame( LINE_JOINER.join( "function f(x) {", " x.b.c = 1;", " var y = x.a.c;", // x.b.c is read here " x.b.c = 2;", " return x.b.c + y;", "}", "var obj = { c: 123 };", "f({a: obj, b: obj});")); } public void testHook() { testSame( LINE_JOINER.join( "function f(x, pred) {", " var y;", " x.p = 234;", " y = pred ? (x.p = 123) : x.p;", "}")); testSame( LINE_JOINER.join( "function f(x, pred) {", " var y;", " x.p = 234;", " y = pred ? (x.p = 123) : 123;", " return x.p;", "}")); } public void testConditionalExpression() { testSame( LINE_JOINER.join( "function f(x) {", " return (x.p = 2) || (x.p = 3);", // Second assignment will never execute. "}")); testSame( LINE_JOINER.join( "function f(x) {", " return (x.p = 0) && (x.p = 3);", // Second assignment will never execute. "}")); } public void testBrackets() { testSame( LINE_JOINER.join( "function f(x, p) {", " x.prop = 123;", " x[p] = 234;", " return x.prop;", "}")); } public void testFor() { testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " for(;x;) {}", " x.p = 2;", "}")); testSame( LINE_JOINER.join( "function f(x) {", " for(;x;) {", " x.p = 1;", " }", " x.p = 2;", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " for(;;) {", " x.p = 2;", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " for(x.p = 2;;) {", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " for(x.p = 2;;x.p=3) {", " return x.p;", // Reads the "x.p = 2" assignment. " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " for(;;) {", " x.p = 1;", " x.p = 2;", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " for(;;) {", " }", " x.p = 2;", "}")); } public void testWhile() { testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " while(x);", " x.p = 2;", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " while(1) {", " x.p = 2;", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " while(true) {", " x.p = 1;", " if (random()) continue;", " x.p = 2;", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " while(true) {", " x.p = 1;", " if (random()) break;", " x.p = 2;", " }", "}")); test( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " while(x.p = 2) {", " }", "}"), LINE_JOINER.join( "function f(x) {", " 1;", " while(x.p = 2) {", " }", "}")); test( LINE_JOINER.join( "function f(x) {", " while(true) {", " x.p = 1;", " x.p = 2;", " }", "}"), LINE_JOINER.join( "function f(x) {", " while(true) {", " 1;", " x.p = 2;", " }", "}")); test( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " while(1) {}", " x.p = 2;", "}"), LINE_JOINER.join( "function f(x) {", " 1;", " while(1) {}", " x.p = 2;", "}")); } public void testTry() { testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " try {", " x.p = 2;", " } catch (e) {}", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 1;", " try {", " } catch (e) {", " x.p = 2;", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " try {", " x.p = 1;", " } catch (e) {", " x.p = 2;", " }", "}")); test( LINE_JOINER.join( "function f(x) {", " try {", " x.p = 1;", " x.p = 2;", " } catch (e) {", " }", "}"), LINE_JOINER.join( "function f(x) {", " try {", " 1;", " x.p = 2;", " } catch (e) {", " }", "}")); testSame( LINE_JOINER.join( "function f(x) {", " try {", " x.p = 1;", " maybeThrow();", " x.p = 2;", " } catch (e) {", " }", "}")); testSame( LINE_JOINER.join( "/** @constructor */", "function f() {", " try {", " this.p = 1;", " maybeThrow();", " this.p = 2;", " } catch (e) {", " }", "}")); } public void testThrow() { testSame( LINE_JOINER.join( "function f(x) {", " x.p = 10", " if (random) throw err;", " x.p = 20;", "}")); testSame( LINE_JOINER.join( "function f(x) {", " x.p = 10", " throw err;", " x.p = 20;", "}")); } public void testSwitch() { testSame( LINE_JOINER.join( "function f(x, pred) {", " x.p = 1;", " switch (pred) {", " case 1:", " x.p = 2;", " case 2:", " x.p = 3;", " break;", " default:", " return x.p;", " }", "}")); testSame( LINE_JOINER.join( "function f(x, pred) {", " x.p = 1;", " switch (pred) {", " default:", " x.p = 2;", " }", " x.p = 3;", "}")); testSame( LINE_JOINER.join( "function f(x, pred) {", " x.p = 1;", " switch (pred) {", " default:", " return;", " }", " x.p = 2;", "}")); // For now we don't enter switch statements. testSame( LINE_JOINER.join( "function f(x, pred) {", " switch (pred) {", " default:", " x.p = 2;", " x.p = 3;", " }", "}")); } public void testIf() { test( LINE_JOINER.join( "function f(x, pred) {", " if (pred) {", " x.p = 1;", " x.p = 2;", " return x.p;", " }", "}"), LINE_JOINER.join( "function f(x, pred) {", " if (pred) {", " 1;", " x.p = 2;", " return x.p;", " }", "}")); test( LINE_JOINER.join( "function f(x, pred) {", " x.p = 1;", " if (pred) {}", " x.p = 2;", " return x.p;", "}"), LINE_JOINER.join( "function f(x, pred) {", " 1;", " if (pred) {}", " x.p = 2;", " return x.p;", "}")); testSame( LINE_JOINER.join( "function f(x, pred) {", " if (pred) {", " x.p = 1;", " }", " x.p = 2;", "}")); } public void testCircularPropChain() { testSame( LINE_JOINER.join( "function f(x, y) {", " x.p = {};", " x.p.y.p.z = 10;", " x.p = {};", "}")); } public void testDifferentQualifiedNames() { testSame( LINE_JOINER.join( "function f(x, y) {", " x.p = 10;", " y.p = 11;", "}")); } public void testGetPropContainsNonQualifiedNames() { testSame( LINE_JOINER.join( "function f(x) {", " foo(x).p = 10;", " foo(x).p = 11;", "}")); testSame( LINE_JOINER.join( "function f(x) {", " (x = 10).p = 10;", " (x = 10).p = 11;", "}")); } public void testEs6Constrcutor() { setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015); testSame( LINE_JOINER.join( "class Foo {", " constructor() {", " this.p = 123;", " var z = this.p;", " this.p = 234;", " }", "}")); test( LINE_JOINER.join( "class Foo {", " constructor() {", " this.p = 123;", " this.p = 234;", " }", "}"), LINE_JOINER.join( "class Foo {", " constructor() {", " 123;", " this.p = 234;", " }", "}")); if (ASSUME_CONSTRUCTORS_HAVENT_ESCAPED) { test( LINE_JOINER.join( "class Foo {", " constructor() {", " this.p = 123;", " foo();", " this.p = 234;", " }", "}"), LINE_JOINER.join( "class Foo {", " constructor() {", " 123;", " foo();", " this.p = 234;", " }", "}")); } setAcceptedLanguage(LanguageMode.ECMASCRIPT5); } public void testGetter() { testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; };", "Object.defineProperties(Foo.prototype, {bar: {", " get: function () { return this.enabled ? 'enabled' : 'disabled'; }", "}});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " var f = foo.bar;", " foo.enabled = false;", "}")); testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; };", "Object.defineProperty(Foo, 'bar', {", " get: function () { return this.enabled ? 'enabled' : 'disabled'; }", "});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " var f = foo.bar;", " foo.enabled = false;", "}")); } public void testGetter_afterDeadAssignment() { testSame( LINE_JOINER.join( "function f() {", " var foo = new Foo()", " foo.enabled = true;", " var f = foo.bar;", " foo.enabled = false;", "}", "/** @constructor */ function Foo() { this.enabled = false; };", "Object.defineProperties(Foo.prototype, {bar: {", " get: function () { return this.enabled ? 'enabled' : 'disabled'; }", "}});")); } public void testGetter_onDifferentType() { testSame( LINE_JOINER.join( "/** @constructor */", "function Foo() {", " this.enabled = false;", "};", "Object.defineProperties(", " Foo.prototype, {", " baz: {", " get: function () { return this.enabled ? 'enabled' : 'disabled'; }", " }", " });", "/** @constructor */", "function Bar() {", " this.enabled = false;", " this.baz = 123;", "};", "function f() {", " var bar = new Bar();", " bar.enabled = true;", " var ret = bar.baz;", " bar.enabled = false;", " return ret;", "};") ); } public void testSetter() { testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "Object.defineProperties(Foo.prototype, {bar: {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "}});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}")); testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "Object.defineProperty(Foo, 'bar', {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}")); } public void testEs5Getter() { testSame( LINE_JOINER.join( "var bar = {", " enabled: false,", " get baz() {", " return this.enabled ? 'enabled' : 'disabled';", " }", "};", "function f() {", " bar.enabled = true;", " var ret = bar.baz;", " bar.enabled = false;", " return ret;", "};") ); } public void testEs5Setter() { testSame( LINE_JOINER.join( "var bar = {", " enabled: false,", " set baz(x) {", " this.x = this.enabled ? x * 2 : x;", " }", "};", "function f() {", " bar.enabled = true;", " bar.baz = 10;", " bar.enabled = false;", "};") ); } public void testObjectDefineProperty_aliasedParams() { testSame( LINE_JOINER.join( "function addGetter(obj, propName) {", " Object.defineProperty(obj, propName, {", " get: function() { return this[propName]; }", " });", "};", "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "addGetter(Foo.prototype, 'x');", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); testSame( LINE_JOINER.join( "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function addGetter(obj, propName) {", " Object.defineProperty(obj, propName, {", " get: function() { return this[propName]; }", " });", "};", "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "addGetter(Foo.prototype, 'x');", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); } public void testObjectDefineProperty_aliasedObject() { testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var x = Foo.prototype;", "Object.defineProperty(x, 'bar', {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); } public void testObjectDefineProperty_aliasedPropName() { testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var x = 'bar';", "Object.defineProperty(Foo.prototype, x, {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); test( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var x = 'bar';", "Object.defineProperty(Foo.prototype, x, {", " value: 10", "});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}"), LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var x = 'bar';", "Object.defineProperty(Foo.prototype, x, {", " value: 10", "});", "function f() {", " var foo = new Foo()", " true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " 10;", " x.bar = 20;", "}")); } public void testObjectDefineProperty_aliasedPropSet() { testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var x = {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "};", "Object.defineProperty(Foo.prototype, 'bar', x);", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); } public void testObjectDefineProperties_aliasedPropertyMap() { testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var properties = {bar: {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "}};", "Object.defineProperties(Foo.prototype, properties);", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); testSame( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var properties = {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "};", "Object.defineProperties(Foo.prototype, {bar: properties});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); } public void testObjectDefineProperties_aliasedObject() { test( LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var properties = {bar: {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "}};", "var x = Foo.prototype;", "Object.defineProperties(x, {bar: {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "}});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " foo.enabled = false;", " foo.enabled = true;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}"), LINE_JOINER.join( "/** @constructor */ function Foo() { this.enabled = false; this.x = null; };", "var properties = {bar: {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "}};", "var x = Foo.prototype;", "Object.defineProperties(x, {bar: {", " set: function (x) { this.x = this.enabled ? x * 2 : x; }", "}});", "function f() {", " var foo = new Foo()", " foo.enabled = true;", " foo.bar = 10;", " false;", " foo.enabled = true;", "}", "function z() {", " var x = {};", " x.bar = 10;", " x.bar = 20;", "}")); } public void testPropertyDefinedInExterns() { String externs = LINE_JOINER.join( "var window = {};", "/** @type {number} */ window.innerWidth", "/** @constructor */", "var Image = function() {};", "/** @type {string} */ Image.prototype.src;" ); testSame( externs, LINE_JOINER.join( "function z() {", " window.innerWidth = 10;", " window.innerWidth = 20;", "}"), null); testSame( externs, LINE_JOINER.join( "function z() {", " var img = new Image();", " img.src = '';", " img.src = 'foo.bar';", "}"), null); testSame( externs, LINE_JOINER.join( "function z(x) {", " x.src = '';", " x.src = 'foo.bar';", "}"), null); } public void testJscompInherits() { test( LINE_JOINER.join( "/** @constructor */ function Foo() { this.bar = null; };", "var $jscomp = {};", "$jscomp.inherits = function(x) {", " Object.defineProperty(x, x, x);", "};", "function f() {", " var foo = new Foo()", " foo.bar = 10;", " foo.bar = 20;", "}"), LINE_JOINER.join( "/** @constructor */ function Foo() { this.bar = null; };", "var $jscomp = {};", "$jscomp.inherits = function(x) {", " Object.defineProperty(x, x, x);", "};", "function f() {", " var foo = new Foo()", " 10;", " foo.bar = 20;", "}")); } @Override protected CompilerPass getProcessor(Compiler compiler) { return new DeadPropertyAssignmentElimination(compiler); } }