Skip to content

Commit

Permalink
Merge pull request #1017 from Pauan/tree-shake-pure-es5-classes
Browse files Browse the repository at this point in the history
Removes `new Foo(...)` when Foo is a pure function
  • Loading branch information
Rich-Harris committed Dec 21, 2016
2 parents 44924ec + 0afa3ed commit 94f2ac4
Show file tree
Hide file tree
Showing 25 changed files with 274 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/ast/nodes/CallExpression.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default class CallExpression extends Node {
}

hasEffects ( scope ) {
return callHasEffects( scope, this.callee );
return callHasEffects( scope, this.callee, false );
}

initialise ( scope ) {
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/NewExpression.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import callHasEffects from './shared/callHasEffects.js';

export default class NewExpression extends Node {
hasEffects ( scope ) {
return callHasEffects( scope, this.callee );
return callHasEffects( scope, this.callee, true );
}
}
44 changes: 40 additions & 4 deletions src/ast/nodes/shared/callHasEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,43 @@ import { UNKNOWN } from '../../values.js';

const currentlyCalling = new Set();

function fnHasEffects ( fn ) {
function isES5Function ( node ) {
return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
}

function hasEffectsNew ( node, scope ) {
let inner = node;

if ( inner.type === 'ExpressionStatement' ) {
inner = inner.expression;

if ( inner.type === 'AssignmentExpression' ) {
if ( inner.right.hasEffects( scope ) ) {
return true;

} else {
inner = inner.left;

if ( inner.type === 'MemberExpression' ) {
if ( inner.computed && inner.property.hasEffects( scope ) ) {
return true;

} else {
inner = inner.object;

if ( inner.type === 'ThisExpression' ) {
return false;
}
}
}
}
}
}

return node.hasEffects( scope );
}

function fnHasEffects ( fn, isNew ) {
if ( currentlyCalling.has( fn ) ) return false; // prevent infinite loops... TODO there must be a better way
currentlyCalling.add( fn );

Expand All @@ -14,7 +50,7 @@ function fnHasEffects ( fn ) {
const body = fn.body.type === 'BlockStatement' ? fn.body.body : [ fn.body ];

for ( const node of body ) {
if ( node.hasEffects( scope ) ) {
if ( isNew ? hasEffectsNew( node, scope ) : node.hasEffects( scope ) ) {
currentlyCalling.delete( fn );
return true;
}
Expand All @@ -24,14 +60,14 @@ function fnHasEffects ( fn ) {
return false;
}

export default function callHasEffects ( scope, callee ) {
export default function callHasEffects ( scope, callee, isNew ) {
const values = new Set([ callee ]);

for ( const node of values ) {
if ( node === UNKNOWN ) return true; // err on side of caution

if ( /Function/.test( node.type ) ) {
if ( fnHasEffects( node ) ) return true;
if ( fnHasEffects( node, isNew && isES5Function( node ) ) ) return true;
}

else if ( isReference( node ) ) {
Expand Down
3 changes: 3 additions & 0 deletions test/form/side-effect-es5-classes/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
description: 'omits ES5 classes which are pure (e.g. they only assign to `this`)'
};
34 changes: 34 additions & 0 deletions test/form/side-effect-es5-classes/_expected/amd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
define(function () { 'use strict';

function Bar ( x ) {
console.log( 'side effect' );
this.value = x;
}

function Baz ( x ) {
this[ console.log( 'side effect' ) ] = x;
}

function Qux ( x ) {
this.value = console.log( 'side effect' );
}

function Corge ( x ) {
this.value = x;
}

var Arrow = ( x ) => {
undefined.value = x;
};

console.log( 'before' );

var bar = new Bar(5);
var baz = new Baz(5);
var qux = new Qux(5);
var corge = Corge(5);
var arrow = new Arrow(5);

console.log( 'after' );

});
32 changes: 32 additions & 0 deletions test/form/side-effect-es5-classes/_expected/cjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

function Bar ( x ) {
console.log( 'side effect' );
this.value = x;
}

function Baz ( x ) {
this[ console.log( 'side effect' ) ] = x;
}

function Qux ( x ) {
this.value = console.log( 'side effect' );
}

function Corge ( x ) {
this.value = x;
}

var Arrow = ( x ) => {
undefined.value = x;
};

console.log( 'before' );

var bar = new Bar(5);
var baz = new Baz(5);
var qux = new Qux(5);
var corge = Corge(5);
var arrow = new Arrow(5);

console.log( 'after' );
30 changes: 30 additions & 0 deletions test/form/side-effect-es5-classes/_expected/es.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function Bar ( x ) {
console.log( 'side effect' );
this.value = x;
}

function Baz ( x ) {
this[ console.log( 'side effect' ) ] = x;
}

function Qux ( x ) {
this.value = console.log( 'side effect' );
}

function Corge ( x ) {
this.value = x;
}

var Arrow = ( x ) => {
undefined.value = x;
};

console.log( 'before' );

var bar = new Bar(5);
var baz = new Baz(5);
var qux = new Qux(5);
var corge = Corge(5);
var arrow = new Arrow(5);

console.log( 'after' );
35 changes: 35 additions & 0 deletions test/form/side-effect-es5-classes/_expected/iife.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
(function () {
'use strict';

function Bar ( x ) {
console.log( 'side effect' );
this.value = x;
}

function Baz ( x ) {
this[ console.log( 'side effect' ) ] = x;
}

function Qux ( x ) {
this.value = console.log( 'side effect' );
}

function Corge ( x ) {
this.value = x;
}

var Arrow = ( x ) => {
undefined.value = x;
};

console.log( 'before' );

var bar = new Bar(5);
var baz = new Baz(5);
var qux = new Qux(5);
var corge = Corge(5);
var arrow = new Arrow(5);

console.log( 'after' );

}());
38 changes: 38 additions & 0 deletions test/form/side-effect-es5-classes/_expected/umd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, (function () { 'use strict';

function Bar ( x ) {
console.log( 'side effect' );
this.value = x;
}

function Baz ( x ) {
this[ console.log( 'side effect' ) ] = x;
}

function Qux ( x ) {
this.value = console.log( 'side effect' );
}

function Corge ( x ) {
this.value = x;
}

var Arrow = ( x ) => {
undefined.value = x;
};

console.log( 'before' );

var bar = new Bar(5);
var baz = new Baz(5);
var qux = new Qux(5);
var corge = Corge(5);
var arrow = new Arrow(5);

console.log( 'after' );

})));
3 changes: 3 additions & 0 deletions test/form/side-effect-es5-classes/arrow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export var Arrow = ( x ) => {
this.value = x;
};
4 changes: 4 additions & 0 deletions test/form/side-effect-es5-classes/bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function Bar ( x ) {
console.log( 'side effect' );
this.value = x;
}
3 changes: 3 additions & 0 deletions test/form/side-effect-es5-classes/baz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Baz ( x ) {
this[ console.log( 'side effect' ) ] = x;
}
3 changes: 3 additions & 0 deletions test/form/side-effect-es5-classes/corge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Corge ( x ) {
this.value = x;
}
4 changes: 4 additions & 0 deletions test/form/side-effect-es5-classes/foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function Foo ( x ) {
this.value = x;
this["string property"] = 20;
}
17 changes: 17 additions & 0 deletions test/form/side-effect-es5-classes/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Foo } from './foo';
import { Bar } from './bar';
import { Baz } from './baz';
import { Qux } from './qux';
import { Corge } from './corge';
import { Arrow } from './arrow';

console.log( 'before' );

var foo = new Foo(5);
var bar = new Bar(5);
var baz = new Baz(5);
var qux = new Qux(5);
var corge = Corge(5);
var arrow = new Arrow(5);

console.log( 'after' );
3 changes: 3 additions & 0 deletions test/form/side-effect-es5-classes/qux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Qux ( x ) {
this.value = console.log( 'side effect' );
}
1 change: 1 addition & 0 deletions test/form/unmodified-default-exports/_expected/amd.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
define(function () { 'use strict';

var Foo = function () {
console.log( 'side effect' );
this.isFoo = true;
};

Expand Down
1 change: 1 addition & 0 deletions test/form/unmodified-default-exports/_expected/cjs.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var Foo = function () {
console.log( 'side effect' );
this.isFoo = true;
};

Expand Down
1 change: 1 addition & 0 deletions test/form/unmodified-default-exports/_expected/es.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var Foo = function () {
console.log( 'side effect' );
this.isFoo = true;
};

Expand Down
1 change: 1 addition & 0 deletions test/form/unmodified-default-exports/_expected/iife.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
'use strict';

var Foo = function () {
console.log( 'side effect' );
this.isFoo = true;
};

Expand Down
3 changes: 2 additions & 1 deletion test/form/unmodified-default-exports/_expected/umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
}(this, (function () { 'use strict';

var Foo = function () {
console.log( 'side effect' );
this.isFoo = true;
};

Expand All @@ -16,4 +17,4 @@

var foo = new Foo();

})));
})));
1 change: 1 addition & 0 deletions test/form/unmodified-default-exports/foo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var Foo = function () {
console.log( 'side effect' );
this.isFoo = true;
};

Expand Down
2 changes: 2 additions & 0 deletions test/function/es5-class-called-without-new/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports = {
};
7 changes: 7 additions & 0 deletions test/function/es5-class-called-without-new/foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function Foo ( x ) {
this.value = x;
}

export function Bar ( x ) {
this.value = x;
}
7 changes: 7 additions & 0 deletions test/function/es5-class-called-without-new/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Foo, Bar } from './foo';

assert.equal( new Foo(5).value, 5 );

assert.throws( function () {
Bar(5);
}, /^TypeError: Cannot set property 'value' of undefined$/ );

0 comments on commit 94f2ac4

Please sign in to comment.