├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── package.json ├── test ├── browserify-test.js ├── bundle │ └── index.js └── unreachableBranchVisitors-test.js └── unreachableBranchTransformer.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | /node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "iojs" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | unreachable-branch-transform 2 | ============================ 3 | 4 | [![Build Status](https://travis-ci.org/zertosh/unreachable-branch-transform.svg?branch=master)](https://travis-ci.org/zertosh/unreachable-branch-transform) 5 | 6 | Removes unreachable code branches in `if` statements, ternaries `?`, and logical operations `||` `&&`, where the test is determinable (like comparing two constants). This is similar to what [UglifyJS](https://github.com/mishoo/UglifyJS2)'s "dead_code" compressor option does, but without the extra code transformations. 7 | 8 | When combined with something like [envify](https://github.com/hughsk/envify) and [browserify](https://github.com/substack/node-browserify), you can perform conditional `require` calls without including more code than you need. 9 | 10 | #### Install #### 11 | 12 | ```bash 13 | npm install unreachable-branch-transform 14 | ``` 15 | 16 | #### Example outputs ##### 17 | 18 | ```js 19 | // original 20 | var transport = process.env.TARGET === 'client' ? require('ajax') : require('fs'); 21 | 22 | // after envify 23 | var transport = 'server' === 'client' ? require('ajax') : require('fs'); 24 | // then after unreachable-branch-transform 25 | var transport = require('fs'); 26 | ``` 27 | 28 | ```js 29 | // original 30 | if (process.env.NODE_ENV === 'development') { 31 | console.log('in dev mode'); 32 | } else { 33 | console.log('in some other mode'); 34 | } 35 | 36 | // after envify 37 | if ('production' === 'development') { 38 | console.log('in dev mode'); 39 | } else { 40 | console.log('in some other mode'); 41 | } 42 | 43 | // then after unreachable-branch-transform 44 | { 45 | console.log('in some other mode'); 46 | } 47 | ``` 48 | 49 | #### Usage 50 | 51 | 52 | 53 | * `unreachable-branch-transform` can be used a [browserify](https://github.com/substack/node-browserify) transform. Just include it like any other transform. 54 | * `unreachable-branch-transform` can also be used on raw code by calling the `transform` function exposed by requiring the package. 55 | 56 | #### Frequently asked questions #### 57 | 58 | ##### Why are `undefined` equality references not removed? 59 | 60 | If you have a branch with the format 61 | 62 | ```javascript 63 | if (undefined === 'production') { 64 | /* ... */ 65 | } 66 | ``` 67 | 68 | it will not be removed. Unfortunately, `undefined` is _not_ a constant in older browser runtimes and can be reassigned. In this case, it could be possible that `undefined` does indeed equal `'production`'. 69 | 70 | Credit 71 | ------ 72 | `esmangle-evaluator` is from the [esmangle](https://github.com/Constellation/esmangle) project. 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var recast = require('recast'); 2 | var stream = require('stream'); 3 | var util = require('util'); 4 | 5 | var transformer = require('./unreachableBranchTransformer'); 6 | 7 | module.exports = UBT; 8 | util.inherits(UBT, stream.Transform); 9 | 10 | function UBT(file, opts) { 11 | if (!(this instanceof UBT)) { 12 | return UBT.configure(opts)(file); 13 | } 14 | 15 | stream.Transform.call(this); 16 | this._data = ''; 17 | } 18 | 19 | UBT.prototype._transform = function(buf, enc, cb) { 20 | this._data += buf; 21 | cb(); 22 | }; 23 | 24 | UBT.prototype._flush = function(cb) { 25 | try { 26 | var code = UBT.transform(this._data); 27 | this.push(code); 28 | } catch(err) { 29 | this.emit('error', err); 30 | return; 31 | } 32 | cb(); 33 | }; 34 | 35 | UBT.configure = function(opts) { 36 | var ignores = ['.json'].concat(opts && opts.ignore || []); 37 | 38 | return function(file) { 39 | for (var i = 0; i < ignores.length; i++) { 40 | if (endsWith(file, ignores[i])) { 41 | return stream.PassThrough(); 42 | } 43 | } 44 | 45 | return new UBT(file); 46 | } 47 | }; 48 | 49 | UBT.transform = function(code) { 50 | var ast = transformer(recast.parse(code)); 51 | return recast.print(ast).code; 52 | }; 53 | 54 | function endsWith(str, suffix) { 55 | if (typeof str !== 'string' || typeof suffix !== 'string') { 56 | return false; 57 | } 58 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unreachable-branch-transform", 3 | "version": "0.5.1", 4 | "description": "Browserify transform to remove unreachable code", 5 | "keywords": [ 6 | "browserify", 7 | "browserify-transform", 8 | "transform", 9 | "minify", 10 | "unreachable" 11 | ], 12 | "homepage": "https://github.com/zertosh/unreachable-branch-transform", 13 | "license": "MIT", 14 | "author": "Andres Suarez ", 15 | "files": [ 16 | "README.md", 17 | "index.js", 18 | "unreachableBranchTransformer.js" 19 | ], 20 | "main": "index.js", 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/zertosh/unreachable-branch-transform.git" 24 | }, 25 | "dependencies": { 26 | "esmangle-evaluator": "^1.0.0", 27 | "recast": "^0.11.4" 28 | }, 29 | "devDependencies": { 30 | "browserify": "^13.0.0", 31 | "tap": "^5.7.1" 32 | }, 33 | "scripts": { 34 | "test": "tap test/*.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/browserify-test.js: -------------------------------------------------------------------------------- 1 | var browserify = require('browserify'); 2 | var test = require('tap').test; 3 | var ubt = require('../'); 4 | var vm = require('vm'); 5 | 6 | test('browserify', function (t) { 7 | t.plan(2); 8 | 9 | var b = browserify(); 10 | 11 | b.require(__dirname + '/bundle/index.js'); 12 | b.transform(ubt); 13 | 14 | b.bundle(function (err, src) { 15 | t.error(err); 16 | t.notMatch(/unreachable/, src); 17 | 18 | var c = { 19 | console: { 20 | log: function(msg) { 21 | t.equal(msg, 'reachable'); 22 | } 23 | } 24 | }; 25 | 26 | vm.runInNewContext(src, c); 27 | }); 28 | }); 29 | 30 | test('browserify with configure', function (t) { 31 | t.plan(2); 32 | 33 | var b = browserify(); 34 | 35 | b.require(__dirname + '/bundle/index.js'); 36 | b.transform(ubt.configure()); 37 | 38 | b.bundle(function (err, src) { 39 | t.error(err); 40 | t.notMatch(/unreachable/, src); 41 | 42 | var c = { 43 | console: { 44 | log: function(msg) { 45 | t.equal(msg, 'reachable'); 46 | } 47 | } 48 | }; 49 | 50 | vm.runInNewContext(src, c); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/bundle/index.js: -------------------------------------------------------------------------------- 1 | if (true) { 2 | console.log('reachable'); 3 | } else { 4 | console.log('unreachable'); 5 | } 6 | -------------------------------------------------------------------------------- /test/unreachableBranchVisitors-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var transform = require('../').transform; 3 | 4 | function expectTransform(t) { 5 | return function(code_, result_) { 6 | var code = Array.isArray(code_) ? code_.join('\n') : code_; 7 | var result = Array.isArray(result_) ? result_.join('\n') : result_; 8 | return t.equal(transform(code), result); 9 | }; 10 | } 11 | 12 | 13 | test('unreachable-branch-transform', function(t) { 14 | 15 | //---------------------------------------------------------------------------- 16 | // With side-effects 17 | //---------------------------------------------------------------------------- 18 | t.test('AssignmentExpression', function(t) { 19 | expectTransform(t)( 20 | 'foo || (a = new RegExp("")) && bar();', 21 | 'foo || (a = new RegExp("")) && bar();' 22 | ); 23 | 24 | t.end(); 25 | }); 26 | 27 | 28 | //---------------------------------------------------------------------------- 29 | // ExpressionStatement 30 | //---------------------------------------------------------------------------- 31 | 32 | t.test('ExpressionStatement', function(t) { 33 | expectTransform(t)( 34 | 'true || true;', 35 | 'true;' 36 | ); 37 | 38 | expectTransform(t)( 39 | 'false || console.log();', 40 | 'console.log();' 41 | ); 42 | 43 | expectTransform(t)( 44 | 'true && console.log();', 45 | 'console.log();' 46 | ); 47 | 48 | expectTransform(t)( 49 | 'true || console.log();', 50 | 'true;' 51 | ); 52 | 53 | expectTransform(t)( 54 | '"foo" || console.log();', 55 | '"foo";' 56 | ); 57 | 58 | expectTransform(t)( 59 | '[1] || console.log();', 60 | '[1];' 61 | ); 62 | 63 | expectTransform(t)( 64 | 'false && console.log();', 65 | 'false;' 66 | ); 67 | 68 | expectTransform(t)( 69 | 'null && console.log();', 70 | 'null;' 71 | ); 72 | 73 | expectTransform(t)( 74 | 'void 0 && console.log();', 75 | 'void 0;' 76 | ); 77 | 78 | t.end(); 79 | }); 80 | 81 | 82 | //---------------------------------------------------------------------------- 83 | // VariableDeclarator 84 | //---------------------------------------------------------------------------- 85 | 86 | t.test('VariableDeclarator', function(t) { 87 | 88 | expectTransform(t)( 89 | 'var a = true ? true : true;', 90 | 'var a = true;' 91 | ); 92 | 93 | expectTransform(t)( 94 | 'var a = true ? true : true;', 95 | 'var a = true;' 96 | ); 97 | 98 | expectTransform(t)( 99 | 'var a = false ? true : true;', 100 | 'var a = true;' 101 | ); 102 | 103 | expectTransform(t)( 104 | 'var a = true ? console.log() : true;', 105 | 'var a = console.log();' 106 | ); 107 | 108 | expectTransform(t)( 109 | 'var a = true ? true : console.log();', 110 | 'var a = true;' 111 | ); 112 | 113 | expectTransform(t)( 114 | 'var a = true ? console.log() : console.log();', 115 | 'var a = console.log();' 116 | ); 117 | 118 | expectTransform(t)( 119 | 'var a = "true" ? true : console.log();', 120 | 'var a = true;' 121 | ); 122 | 123 | expectTransform(t)( 124 | 'var a = "true" ? console.log(1) : console.log(2);', 125 | 'var a = console.log(1);' 126 | ); 127 | 128 | expectTransform(t)( 129 | 'var a = "" ? console.log() : true;', 130 | 'var a = true;' 131 | ); 132 | 133 | expectTransform(t)( 134 | 'var a = "" ? console.log(1) : console.log(2);', 135 | 'var a = console.log(2);' 136 | ); 137 | 138 | expectTransform(t)( 139 | 'var a = "production" !== "development" ? console.log(3) : console.log(4);', 140 | 'var a = console.log(3);' 141 | ); 142 | 143 | expectTransform(t)( 144 | 'var a = "production" === "development" ? console.log(5) : console.log(6);', 145 | 'var a = console.log(6);' 146 | ); 147 | 148 | t.end(); 149 | }); 150 | 151 | //---------------------------------------------------------------------------- 152 | // IfStatement 153 | //---------------------------------------------------------------------------- 154 | 155 | t.test('IfStatement', function(t) { 156 | 157 | expectTransform(t)( 158 | 'if (true) { console.log(1); } else { console.log(2); }', 159 | '{ console.log(1); }' 160 | ); 161 | 162 | expectTransform(t)( 163 | 'if (false) { console.log(1); } else { console.log(2); }', 164 | '{ console.log(2); }' 165 | ); 166 | 167 | expectTransform(t)([ 168 | 'if (true) {', 169 | ' console.log(1);', 170 | '} else if (true) {', 171 | ' console.log(2);', 172 | '} else {', 173 | ' console.log(3);', 174 | '}' 175 | ], [ 176 | '{', 177 | ' console.log(1);', 178 | '}' 179 | ]); 180 | 181 | expectTransform(t)([ 182 | 'if (false) {', 183 | ' console.log(1);', 184 | '} else if (true) {', 185 | ' console.log(2);', 186 | '} else {', 187 | ' console.log(3);', 188 | '}' 189 | ], [ 190 | '{', 191 | ' console.log(2);', 192 | '}' 193 | ]); 194 | 195 | expectTransform(t)([ 196 | 'if (false)', 197 | ' console.log(1);', 198 | 'else if (true)', 199 | ' console.log(2);', 200 | 'else', 201 | ' console.log(3);', 202 | ], [ 203 | 'console.log(2);' 204 | ]); 205 | 206 | expectTransform(t)([ 207 | 'if (false || false || func()) {', 208 | ' console.log(1);', 209 | '} else if (false ? true : (func())) {', 210 | ' console.log(2);', 211 | '} else {', 212 | ' console.log(3);', 213 | '}' 214 | ], [ 215 | 'if (func()) {', 216 | ' console.log(1);', 217 | '} else if (func()) {', 218 | ' console.log(2);', 219 | '} else {', 220 | ' console.log(3);', 221 | '}' 222 | ]); 223 | 224 | t.end(); 225 | }); 226 | 227 | t.end(); 228 | }); 229 | -------------------------------------------------------------------------------- /unreachableBranchTransformer.js: -------------------------------------------------------------------------------- 1 | var Evaluator = require('esmangle-evaluator'); 2 | 3 | var recast = require('recast'); 4 | var types = recast.types; 5 | var b = types.builders; 6 | 7 | var VISITOR_METHODS = { 8 | visitLogicalExpression: visitLogicalExp, 9 | visitIfStatement: visitCondition, 10 | visitConditionalExpression: visitCondition 11 | }; 12 | 13 | module.exports = function(branch) { 14 | recast.visit(branch, VISITOR_METHODS); 15 | return branch; 16 | }; 17 | 18 | 19 | /** 20 | * "||" and "&&" 21 | */ 22 | function visitLogicalExp(path) { 23 | var leftEval = Evaluator.booleanCondition(path.node.left); 24 | 25 | if (typeof leftEval !== 'boolean') { 26 | // console.log('___ %s ___', path.node.operator); 27 | this.traverse(path); 28 | return; 29 | } 30 | 31 | var leftSideEffect = Evaluator.hasSideEffect(path.node.left); 32 | if (leftSideEffect) { 33 | this.traverse(path); 34 | return; 35 | } 36 | 37 | if (leftEval === true && path.node.operator === '||') { 38 | // console.log('true || ___'); 39 | path.replace(path.node.left); 40 | recast.visit(path, VISITOR_METHODS); 41 | return false; 42 | } 43 | 44 | if (leftEval === true && path.node.operator === '&&') { 45 | // console.log('true && ___'); 46 | path.replace(path.node.right); 47 | recast.visit(path, VISITOR_METHODS); 48 | return false; 49 | } 50 | 51 | if (leftEval === false && path.node.operator === '&&') { 52 | // console.log('false && ___'); 53 | path.replace(path.node.left); 54 | recast.visit(path, VISITOR_METHODS); 55 | return false; 56 | } 57 | 58 | if (leftEval === false && path.node.operator === '||') { 59 | // console.log('false || ___'); 60 | path.replace(path.node.right); 61 | recast.visit(path, VISITOR_METHODS); 62 | return false; 63 | } 64 | } 65 | 66 | /** 67 | * "if" and ternary "?" 68 | */ 69 | function visitCondition(path) { 70 | var testEval = Evaluator.booleanCondition(path.node.test); 71 | 72 | if (typeof testEval !== 'boolean') { 73 | // console.log('if/? ___'); 74 | this.traverse(path); 75 | return; 76 | } 77 | 78 | var testSideEffect = Evaluator.hasSideEffect(path.node.test); 79 | if (testSideEffect) { 80 | this.traverse(path); 81 | return; 82 | } 83 | 84 | if (testEval === true) { 85 | // console.log('if/? (true)'); 86 | path.replace(path.value.consequent); 87 | recast.visit(path, VISITOR_METHODS); 88 | return false; 89 | } 90 | 91 | if (testEval === false) { 92 | // console.log('if/? (false)'); 93 | path.replace(path.value.alternate); 94 | recast.visit(path, VISITOR_METHODS); 95 | return false; 96 | } 97 | } 98 | --------------------------------------------------------------------------------