From b16921646d076e0cb8b0912e3ca28c62eb016d0a Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Fri, 30 Oct 2015 22:54:15 -0400 Subject: [PATCH] exclude dead branches --- src/Module.js | 13 ++++ src/ast/conditions.js | 65 +++++++++++++++++++ src/ast/create.js | 7 ++ .../function/skips-dead-branches-b/_config.js | 8 +++ test/function/skips-dead-branches-b/main.js | 10 +++ .../function/skips-dead-branches-c/_config.js | 8 +++ test/function/skips-dead-branches-c/main.js | 10 +++ .../function/skips-dead-branches-d/_config.js | 8 +++ test/function/skips-dead-branches-d/main.js | 10 +++ .../function/skips-dead-branches-e/_config.js | 8 +++ test/function/skips-dead-branches-e/main.js | 10 +++ test/function/skips-dead-branches/_config.js | 8 +++ test/function/skips-dead-branches/main.js | 10 +++ test/test.js | 2 + 14 files changed, 177 insertions(+) create mode 100644 src/ast/conditions.js create mode 100644 src/ast/create.js create mode 100644 test/function/skips-dead-branches-b/_config.js create mode 100644 test/function/skips-dead-branches-b/main.js create mode 100644 test/function/skips-dead-branches-c/_config.js create mode 100644 test/function/skips-dead-branches-c/main.js create mode 100644 test/function/skips-dead-branches-d/_config.js create mode 100644 test/function/skips-dead-branches-d/main.js create mode 100644 test/function/skips-dead-branches-e/_config.js create mode 100644 test/function/skips-dead-branches-e/main.js create mode 100644 test/function/skips-dead-branches/_config.js create mode 100644 test/function/skips-dead-branches/main.js diff --git a/src/Module.js b/src/Module.js index 3bbf32e..159f466 100644 --- a/src/Module.js +++ b/src/Module.js @@ -7,6 +7,8 @@ import { basename, extname } from './utils/path.js'; import getLocation from './utils/getLocation.js'; import makeLegalIdentifier from './utils/makeLegalIdentifier.js'; import SOURCEMAPPING_URL from './utils/sourceMappingURL.js'; +import { isFalsy, isTruthy } from './ast/conditions.js'; +import { emptyBlockStatement } from './ast/create.js'; class SyntheticDefaultDeclaration { constructor ( node, statement, name ) { @@ -446,6 +448,17 @@ export default class Module { walk( ast, { enter: node => { + // eliminate dead branches early + if ( node.type === 'IfStatement' ) { + if ( isFalsy( node.test ) ) { + this.magicString.overwrite( node.consequent.start, node.consequent.end, '{}' ); + node.consequent = emptyBlockStatement( node.consequent.start, node.consequent.end ); + } else if ( node.alternate && isTruthy( node.test ) ) { + this.magicString.overwrite( node.alternate.start, node.alternate.end, '{}' ); + node.alternate = emptyBlockStatement( node.alternate.start, node.alternate.end ); + } + } + this.magicString.addSourcemapLocation( node.start ); this.magicString.addSourcemapLocation( node.end ); } diff --git a/src/ast/conditions.js b/src/ast/conditions.js new file mode 100644 index 0000000..e3d905f --- /dev/null +++ b/src/ast/conditions.js @@ -0,0 +1,65 @@ +function isEqualTest ( node ) { + return node.type === 'BinaryExpression' && ( node.operator === '===' || node.operator === '==' ); +} + +function isNotEqualTest ( node ) { + return node.type === 'BinaryExpression' && ( node.operator === '!==' || node.operator === '!=' ); +} + +function nodesAreEqual ( a, b ) { + if ( a.type !== b.type ) return false; + if ( a.type === 'Literal' ) return a.value === b.value; + if ( a.type === 'Identifier' ) return a.name === b.name; + + return false; +} + +function nodesAreNotEqual ( a, b ) { + if ( a.type !== b.type ) return false; + if ( a.type === 'Literal' ) return a.value != b.value; + if ( a.type === 'Identifier' ) return a.name != b.name; + + return false; +} + +export function isTruthy ( node ) { + if ( node.type === 'Literal' && node.value ) return true; + if ( node.type === 'ParenthesizedExpression' ) return isTruthy( node.expression ); + + if ( isEqualTest( node ) ) return nodesAreEqual( node.left, node.right ); + if ( isNotEqualTest( node ) ) return nodesAreNotEqual( node.left, node.right ); + + if ( node.type === 'UnaryExpression' ) { + if ( node.operator === '!' ) return isFalsy( node.argument ); + return false; + } + + if ( node.type === 'LogicalExpression' ) { + if ( node.operator === '&&' ) return isTruthy( node.left ) && isTruthy( node.right ); + if ( node.operator === '||' ) return isTruthy( node.left ) || isTruthy( node.right ); + return false; + } + + return false; +} + +export function isFalsy ( node ) { + if ( node.type === 'Literal' && !node.value ) return true; + if ( node.type === 'ParenthesizedExpression' ) return isFalsy( node.expression ); + + if ( isEqualTest( node ) ) return nodesAreNotEqual( node.left, node.right ); + if ( isNotEqualTest( node ) ) return nodesAreEqual( node.left, node.right ); + + if ( node.type === 'UnaryExpression' ) { + if ( node.operator === '!' ) return isTruthy( node.argument ); + return false; + } + + if ( node.type === 'LogicalExpression' ) { + if ( node.operator === '&&' ) return isFalsy( node.left ) || isFalsy( node.right ); + if ( node.operator === '||' ) return isFalsy( node.left ) && isFalsy( node.right ); + return false; + } + + return false; +} diff --git a/src/ast/create.js b/src/ast/create.js new file mode 100644 index 0000000..e767dbd --- /dev/null +++ b/src/ast/create.js @@ -0,0 +1,7 @@ +export function emptyBlockStatement ( start, end ) { + return { + start, end, + type: 'BlockStatement', + body: [] + }; +} diff --git a/test/function/skips-dead-branches-b/_config.js b/test/function/skips-dead-branches-b/_config.js new file mode 100644 index 0000000..3bdcfdd --- /dev/null +++ b/test/function/skips-dead-branches-b/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'skips a dead branch (b)', + code: function ( code ) { + assert.equal( code.indexOf( 'function foo' ), -1, code ); + } +} diff --git a/test/function/skips-dead-branches-b/main.js b/test/function/skips-dead-branches-b/main.js new file mode 100644 index 0000000..fd91f85 --- /dev/null +++ b/test/function/skips-dead-branches-b/main.js @@ -0,0 +1,10 @@ +function foo () { + console.log( 'this should be excluded' ); +} + +function bar () { + console.log( 'this should be included' ); +} + +if ( true ) bar(); +else foo(); diff --git a/test/function/skips-dead-branches-c/_config.js b/test/function/skips-dead-branches-c/_config.js new file mode 100644 index 0000000..2d4a166 --- /dev/null +++ b/test/function/skips-dead-branches-c/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'skips a dead branch (c)', + code: function ( code ) { + assert.equal( code.indexOf( 'function foo' ), -1, code ); + } +} diff --git a/test/function/skips-dead-branches-c/main.js b/test/function/skips-dead-branches-c/main.js new file mode 100644 index 0000000..55460e9 --- /dev/null +++ b/test/function/skips-dead-branches-c/main.js @@ -0,0 +1,10 @@ +function foo () { + console.log( 'this should be excluded' ); +} + +function bar () { + console.log( 'this should be included' ); +} + +if ( !true ) foo(); +bar(); diff --git a/test/function/skips-dead-branches-d/_config.js b/test/function/skips-dead-branches-d/_config.js new file mode 100644 index 0000000..2d4a166 --- /dev/null +++ b/test/function/skips-dead-branches-d/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'skips a dead branch (c)', + code: function ( code ) { + assert.equal( code.indexOf( 'function foo' ), -1, code ); + } +} diff --git a/test/function/skips-dead-branches-d/main.js b/test/function/skips-dead-branches-d/main.js new file mode 100644 index 0000000..9e4894c --- /dev/null +++ b/test/function/skips-dead-branches-d/main.js @@ -0,0 +1,10 @@ +function foo () { + console.log( 'this should be excluded' ); +} + +function bar () { + console.log( 'this should be included' ); +} + +if ( 'development' === 'production' ) foo(); +bar(); diff --git a/test/function/skips-dead-branches-e/_config.js b/test/function/skips-dead-branches-e/_config.js new file mode 100644 index 0000000..2d4a166 --- /dev/null +++ b/test/function/skips-dead-branches-e/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'skips a dead branch (c)', + code: function ( code ) { + assert.equal( code.indexOf( 'function foo' ), -1, code ); + } +} diff --git a/test/function/skips-dead-branches-e/main.js b/test/function/skips-dead-branches-e/main.js new file mode 100644 index 0000000..bf8e903 --- /dev/null +++ b/test/function/skips-dead-branches-e/main.js @@ -0,0 +1,10 @@ +function foo () { + console.log( 'this should be excluded' ); +} + +function bar () { + console.log( 'this should be included' ); +} + +if ( 'production' !== 'production' ) foo(); +bar(); diff --git a/test/function/skips-dead-branches/_config.js b/test/function/skips-dead-branches/_config.js new file mode 100644 index 0000000..aacc756 --- /dev/null +++ b/test/function/skips-dead-branches/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'skips a dead branch', + code: function ( code ) { + assert.equal( code.indexOf( 'function foo' ), -1, code ); + } +} diff --git a/test/function/skips-dead-branches/main.js b/test/function/skips-dead-branches/main.js new file mode 100644 index 0000000..2b44506 --- /dev/null +++ b/test/function/skips-dead-branches/main.js @@ -0,0 +1,10 @@ +function foo () { + console.log( 'this should be excluded' ); +} + +function bar () { + console.log( 'this should be included' ); +} + +if ( false ) foo(); +else bar(); diff --git a/test/test.js b/test/test.js index cdb2952..83ed90e 100644 --- a/test/test.js +++ b/test/test.js @@ -161,6 +161,8 @@ describe( 'rollup', function () { code = result.code; } + if ( config.code ) config.code( code ); + var module = { exports: {} };