├── .npmignore ├── .babelrc ├── utils ├── unpad.js ├── dump-scope.js └── test-transform.js ├── src ├── __tests__ │ └── preset.js ├── index.js └── plugins │ ├── object-unfreeze.js │ ├── __tests__ │ ├── object-unfreeze.js │ ├── inline-identity.js │ ├── flatten-iife.js │ ├── store-to-load.js │ └── scalar-replacement.js │ ├── inline-identity.js │ ├── store-to-load.js │ ├── flatten-iife.js │ └── scalar-replacement.js ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── .gitignore ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | # Everything 2 | * 3 | 4 | !dist/**/* 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-destructuring", 4 | "transform-es2015-parameters", 5 | ["transform-es2015-block-scoping", { "throwIfClosureRequired": true }] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /utils/unpad.js: -------------------------------------------------------------------------------- 1 | // https://github.com/babel/minify 2 | // MIT License 3 | 4 | // Remove padding from a string. 5 | function unpad(str) { 6 | const lines = str.split("\n"); 7 | const m = lines[1] && lines[1].match(/^\s+/); 8 | if (!m) { 9 | return str; 10 | } 11 | const spaces = m[0].length; 12 | return lines 13 | .map(line => line.slice(spaces)) 14 | .join("\n") 15 | .trim(); 16 | } 17 | 18 | module.exports = unpad; 19 | -------------------------------------------------------------------------------- /src/__tests__/preset.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const thePreset = require('../../utils/test-transform')([], { presets: [require('../index')] }); 4 | 5 | describe('more-optimization', () => { 6 | thePreset('cuts through exports objects', ` 7 | function foo() {} 8 | function bar() {} 9 | 10 | const exports = Object.freeze({ 11 | a: foo, 12 | b: { c: bar } 13 | }); 14 | 15 | const f_ = exports.a; 16 | const b_ = exports.b.c; 17 | 18 | function main() { 19 | f_(); 20 | b_(); 21 | } 22 | `, ` 23 | function foo() {} 24 | function bar() {} 25 | 26 | function main() { 27 | foo(); 28 | bar(); 29 | } 30 | `); 31 | }); 32 | -------------------------------------------------------------------------------- /utils/dump-scope.js: -------------------------------------------------------------------------------- 1 | // modified from scope.dump() 2 | // https://github.com/babel/babel/blob/6560a29c36fd0f9ef84e78738de11e0477b1384f/packages/babel-traverse/src/scope/index.js#L385 3 | module.exports = function dumpScope(scope) { 4 | const rows = []; 5 | do { 6 | rows.push(scope.block.type); 7 | for (const name in scope.bindings) { 8 | const binding = scope.bindings[name]; 9 | rows.push(`- ${name} ${JSON.stringify({ 10 | constant: binding.constant, 11 | references: binding.references, 12 | violations: binding.constantViolations.length, 13 | kind: binding.kind, 14 | })}`); 15 | } 16 | } while ((scope = scope.parent)); 17 | return rows.join('\n'); 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v*.*.* 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '12.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | - run: npm install 21 | - run: npm run coverage 22 | - uses: actions/upload-artifact@v1 23 | with: 24 | name: coverage 25 | path: coverage/lcov-report 26 | - run: npm publish 27 | if: startsWith(github.ref, 'refs/tags/') 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const flattenIifePlugin = require('./plugins/flatten-iife'); 2 | const inlineIdentityPlugin = require('./plugins/inline-identity'); 3 | const objectUnfreezePlugin = require('./plugins/object-unfreeze'); 4 | const scalarReplacementPlugin = require('./plugins/scalar-replacement'); 5 | const storeToLoadPlugin = require('./plugins/store-to-load'); 6 | 7 | module.exports = function babelPresetMoreOptimization(context, opts_) { 8 | const opts = opts_ || {}; 9 | 10 | const pluginOpts = { 11 | unsafe: opts.unsafe || false, 12 | debug: opts.debug || false 13 | }; 14 | 15 | return { 16 | passPerPreset: true, 17 | presets: [ 18 | { plugins: [[flattenIifePlugin, pluginOpts]] }, 19 | { plugins: [[inlineIdentityPlugin, pluginOpts]] }, 20 | { plugins: [[objectUnfreezePlugin, pluginOpts]] }, 21 | { plugins: [[scalarReplacementPlugin, pluginOpts]] }, 22 | { plugins: [[storeToLoadPlugin, pluginOpts]] } 23 | ] 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Erik Desjardins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # IntelliJ 61 | *.iml 62 | .idea 63 | 64 | dist/ 65 | coverage/ 66 | -------------------------------------------------------------------------------- /src/plugins/object-unfreeze.js: -------------------------------------------------------------------------------- 1 | // Remove calls to Object.freeze, Object.seal, Object.preventExtensions 2 | // ...with the aim of facilitating SROA (primarily), DCE, and inlining. 3 | // Currently, these functions also hurt performance, by converting the objects to a slower backing store (in V8), 4 | // however in the future this may change (V8 will optimize for nonconfigurable nonwriteable properties in context specialization), 5 | // so it may be beneficial to remove this and just make SROA "see through" these functions. 6 | 7 | module.exports = function storeToLoadPlugin({ types: t }) { 8 | return { 9 | visitor: { 10 | CallExpression(path) { 11 | // limit to `Object.{freeze,seal,preventExtensions}()` 12 | if (!t.isMemberExpression(path.node.callee) || path.node.callee.computed) return; 13 | if (!t.isIdentifier(path.node.callee.object, { name: 'Object' })) return; 14 | if (['freeze', 'seal', 'preventExtensions'].indexOf(path.node.callee.property.name) === -1) return; 15 | if (path.node.arguments.length !== 1) return; 16 | 17 | // ensure that Object is not shadowed 18 | if (path.scope.getBinding('Object')) return; 19 | 20 | path.replaceWith(path.node.arguments[0]); 21 | } 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/plugins/__tests__/object-unfreeze.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const thePlugin = require('../../../utils/test-transform')(require('../object-unfreeze')); 4 | 5 | describe('object-unfreeze', () => { 6 | describe('succeeds on', () => { 7 | thePlugin('basic', ` 8 | Object.freeze({}); 9 | Object.seal({}); 10 | Object.preventExtensions({}); 11 | `, ` 12 | ({}); 13 | ({}); 14 | ({}); 15 | `); 16 | thePlugin('other cases', ` 17 | ({ 18 | foo: Object.freeze({}) 19 | }); 20 | foo(Object.seal({})); 21 | var abc = Object.preventExtensions({}); 22 | `, ` 23 | ({ 24 | foo: {} 25 | }); 26 | foo({}); 27 | var abc = {}; 28 | `); 29 | }); 30 | 31 | describe('bails out on', () => { 32 | thePlugin('shadowed Object', ` 33 | var Object = {}; 34 | Object.freeze({}); 35 | `); 36 | thePlugin('shadowed Object, child scope', ` 37 | var Object = {}; 38 | function foo() { 39 | Object.freeze({}); 40 | } 41 | `); 42 | thePlugin('non-calls', ` 43 | Object.freeze; 44 | foo(Object.freeze); 45 | Object.freeze.foo(); 46 | `); 47 | thePlugin('different member functions', ` 48 | Object.defineProperty({}); 49 | `); 50 | thePlugin('computed member functions', ` 51 | Object[foo]({}); 52 | `); 53 | thePlugin('not a single argument', ` 54 | Object.freeze(); 55 | Object.freeze({}, 42); 56 | `); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-preset-more-optimization", 3 | "version": "0.0.6", 4 | "description": "Babel preset for additional optimization/minification.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "babel src --ignore __tests__ --out-dir dist", 8 | "test": "jest", 9 | "watch": "jest --watch", 10 | "coverage": "jest --coverage --collectCoverageFrom=src/**/*.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/erikdesjardins/babel-preset-more-optimization.git" 15 | }, 16 | "keywords": [ 17 | "babel" 18 | ], 19 | "author": "Erik Desjardins", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/erikdesjardins/babel-preset-more-optimization/issues" 23 | }, 24 | "homepage": "https://github.com/erikdesjardins/babel-preset-more-optimization#readme", 25 | "jest": { 26 | "coverageThreshold": { 27 | "global": { 28 | "lines": 100 29 | } 30 | } 31 | }, 32 | "devDependencies": { 33 | "babel-cli": "^6.26.0", 34 | "babel-core": "^6.26.0", 35 | "babel-jest": "^21.2.0", 36 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 37 | "babel-plugin-transform-es2015-block-scoping": "^6.26.0", 38 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 39 | "babel-plugin-transform-es2015-parameters": "^6.24.1", 40 | "babel-register": "^6.26.0", 41 | "jest": "^21.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /utils/test-transform.js: -------------------------------------------------------------------------------- 1 | // https://github.com/babel/minify 2 | // MIT License 3 | 4 | const babel = require("babel-core"); 5 | 6 | const unpad = require("./unpad"); 7 | 8 | function transform(source, options) { 9 | return babel.transform(unpad(source), options).code.trim(); 10 | } 11 | 12 | function makeTester(plugins, opts, check) { 13 | if (!Array.isArray(plugins)) { 14 | plugins = [plugins]; 15 | } 16 | const thePlugin = (name, source, expected = source) => { 17 | const { stack } = new Error(); 18 | const options = Object.assign( 19 | { plugins: plugins.concat("syntax-object-rest-spread"), sourceType: "script" }, 20 | opts, 21 | ); 22 | it(name, () => { 23 | const transformed = transform(source, options); 24 | try { 25 | check({ 26 | transformed, 27 | expected: unpad(expected), 28 | source: unpad(source) 29 | }); 30 | } catch (e) { 31 | // use the stack from outside the it() clause 32 | // (the one inside the clause doesn’t show the actual test code) 33 | e.stack = stack; 34 | throw e; 35 | } 36 | }); 37 | }; 38 | thePlugin.skip = name => it.skip(name); 39 | return thePlugin; 40 | } 41 | 42 | module.exports = (plugins, opts) => 43 | makeTester(plugins, opts, ({ transformed, expected }) => { 44 | expect(transformed).toBe(expected); 45 | }); 46 | 47 | module.exports.withVerifier = (plugins) => 48 | (name, verifierVisitor, source, expected) => makeTester(plugins, { 49 | passPerPreset: true, 50 | presets: [{ plugins: [() => ({ visitor: { Program: { exit: verifierVisitor } } })] }], 51 | }, ({ transformed, expected }) => { 52 | expect(transformed).toBe(expected); 53 | })(name, source, expected); 54 | 55 | module.exports.snapshot = (plugins, opts) => 56 | makeTester(plugins, opts, ({ transformed, source }) => { 57 | // Jest arranges in alphabetical order, So keeping it as _source 58 | expect({ _source: source, expected: transformed }).toMatchSnapshot(); 59 | }); 60 | -------------------------------------------------------------------------------- /src/plugins/inline-identity.js: -------------------------------------------------------------------------------- 1 | // Inline trivial identity functions 2 | 3 | module.exports = function inlineIdentityPlugin({ types: t }) { 4 | return { 5 | visitor: { 6 | CallExpression(path) { 7 | const { unsafe } = this.opts; 8 | 9 | const { callee } = path.node; 10 | 11 | if (!t.isIdentifier(callee)) return; // IIFE or something 12 | 13 | // check for impure arguments, except the first (forwarded) argument, which is fine 14 | for (const arg of path.get('arguments').slice(1)) { 15 | if (!arg.isPure()) { 16 | if (unsafe && t.isIdentifier(arg)) continue; // unreferenced identifiers which could throw 17 | return; // impure additional arguments 18 | } 19 | } 20 | 21 | // check if callee is a function 22 | const binding = path.scope.getBinding(callee.name); 23 | if (!binding || !binding.constant) return; 24 | let fn; 25 | if (t.isFunctionDeclaration(binding.path.node)) { 26 | fn = binding.path.node; 27 | } else if (t.isVariableDeclarator(binding.path.node)) { 28 | if (!t.isFunction(binding.path.node.init)) return; // something weird 29 | fn = binding.path.node.init; 30 | } else { 31 | return; // class or something 32 | } 33 | 34 | // check if function is an identity function 35 | if (t.isIdentifier(fn.body)) { 36 | // expression-body arrow function 37 | if (fn.params.length === 0 || 38 | fn.params[0].name !== fn.body.name) return; // returns a different identifier 39 | } else if (t.isBlock(fn.body)) { 40 | if (fn.body.body.length === 0 || 41 | !t.isReturnStatement(fn.body.body[0]) || 42 | !t.isIdentifier(fn.body.body[0].argument) || 43 | fn.params.length === 0 || 44 | fn.params[0].name !== fn.body.body[0].argument.name) return; // doesn't return the first argument 45 | } else { 46 | return; // something weird, probably unreachable 47 | } 48 | 49 | // replace with first argument or void expression 50 | path.replaceWith(path.get('arguments')[0] || t.unaryExpression('void', t.numericLiteral(0))); 51 | } 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/plugins/store-to-load.js: -------------------------------------------------------------------------------- 1 | // Store-to-load forwarding, i.e. copy propagation 2 | // Only constant->constant copies are propagated, 3 | // as tracking mutation would require more complex dataflow analysis. 4 | 5 | module.exports = function storeToLoadPlugin({ types: t }) { 6 | return { 7 | visitor: { 8 | VariableDeclarator(path) { 9 | if (!t.isIdentifier(path.node.id)) return; // not a pure store (destructuring) 10 | if (!t.isIdentifier(path.node.init)) return; // not a pure load 11 | 12 | const storeBinding = path.scope.getBinding(path.node.id.name); 13 | if (!storeBinding.constant) return; // store is overwritten 14 | 15 | const loadBinding = path.scope.getBinding(path.node.init.name); 16 | if (!loadBinding || !loadBinding.constant) return; // value loaded is overwritten 17 | 18 | let renameTo = path.node.init.name; 19 | 20 | // both references of the redundant identifier and the original (forwarded) identifier 21 | // must be checked, to ensure that we won't be renaming existing references to a shadowed name 22 | const relevantReferences = [].concat(storeBinding.referencePaths, loadBinding.referencePaths); 23 | 24 | // ensure that binding will be unique in all relevant child scopes 25 | for (let i = 0; i < relevantReferences.length; ++i) { 26 | const binding = relevantReferences[i].scope.getBinding(renameTo); 27 | // if binding binding resolves to something, try a different identifier 28 | // unless it's the original (forwarded) identifier, which is fine 29 | if (binding && binding !== loadBinding) { 30 | renameTo = path.scope.generateUid(path.node.init.name); 31 | 32 | // replace original definition 33 | loadBinding.path.node.id.name = renameTo; 34 | 35 | // restart iteration 36 | i = -1; 37 | } 38 | } 39 | 40 | // replace references 41 | for (const reference of relevantReferences) { 42 | reference.node.name = renameTo; 43 | } 44 | 45 | // remove forwarded-out variable and repair scope information 46 | path.remove(); 47 | path.scope.crawl(); // record the removal of that variable 48 | 49 | // repair top-level scope information 50 | loadBinding.scope.crawl(); // record new references of original (forwarded) variable 51 | } 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/plugins/flatten-iife.js: -------------------------------------------------------------------------------- 1 | // Flatten trivial IIFEs 2 | 3 | module.exports = function scalarReplacementPlugin({ types: t }) { 4 | return { 5 | visitor: { 6 | CallExpression(path) { 7 | const { callee, arguments: _arguments } = path.node; 8 | 9 | if (_arguments.length > 0) return; // arguments 10 | 11 | if (!t.isFunction(callee)) return; // not a function 12 | if (callee.async || callee.generator) return; // async or generator 13 | if (Object.keys(path.get('callee').scope.bindings).length > 0) return; // bindings 14 | 15 | // ensure that `this` and `arguments` are not referenced from within the IIFE 16 | let badReference = false; 17 | path.get('callee').traverse({ 18 | ThisExpression(innerPath) { 19 | badReference = true; 20 | innerPath.stop(); 21 | }, 22 | Identifier(innerPath) { 23 | if (innerPath.node.name === 'arguments') { 24 | badReference = true; 25 | innerPath.stop(); 26 | } 27 | } 28 | }); 29 | if (badReference) return; 30 | 31 | // select statements up to the first return 32 | let statements = []; 33 | let foundReturnStatement = false; 34 | for (const statement of (t.isBlockStatement(callee.body) ? callee.body.body : [t.returnStatement(callee.body)])) { 35 | if (t.isExpressionStatement(statement)) { 36 | statements.push(statement); 37 | } else if (t.isReturnStatement(statement)) { 38 | foundReturnStatement = true; 39 | statements.push(t.expressionStatement(statement.argument)); 40 | break; // everything after is dead code 41 | } else { 42 | return; // not an expression statement or a return 43 | } 44 | } 45 | 46 | // replace IIFE 47 | if (t.isExpressionStatement(path.parent)) { 48 | // expression statement; we can emit inner statements as-is 49 | path.parentPath.replaceWithMultiple(statements); 50 | } else { 51 | // expression context; we must emit inner statements as a sequence expression 52 | if (!foundReturnStatement) { 53 | // add implicit return 54 | statements.push(t.expressionStatement(t.unaryExpression('void', t.numericLiteral(0)))); 55 | } 56 | 57 | // pull values out of each expression statement 58 | const innerStatements = statements.map(statement => statement.expression); 59 | 60 | path.replaceWith(innerStatements.length === 1 ? innerStatements[0] : t.sequenceExpression(innerStatements)); 61 | } 62 | } 63 | } 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-preset-more-optimization 2 | 3 | Babel preset for additional optimization/minification. 4 | 5 | ## If you stumble across this: NOT FOR PRODUCTION USE 6 | 7 | ## Installation 8 | 9 | `npm install --save-dev babel-preset-more-optimization` 10 | 11 | ## Usage 12 | 13 | **.babelrc:** 14 | 15 | ```json 16 | { 17 | "presets": [ 18 | "more-optimization" 19 | ] 20 | } 21 | ``` 22 | 23 | Or, with the `unsafe` option: 24 | 25 | ```json 26 | { 27 | "presets": [ 28 | ["more-optimization", { "unsafe": true }] 29 | ] 30 | } 31 | ``` 32 | 33 | ...its behaviour is described in the following sections. 34 | 35 | ## Optimizations 36 | 37 | ### Flatten trivial IIFEs 38 | 39 | In: 40 | 41 | ```js 42 | (() => { 43 | foo(); 44 | bar.baz(); 45 | })(); 46 | 47 | var x = (function() { 48 | return 'x'; 49 | })(); 50 | 51 | (function() { 52 | var someBinding; 53 | if (or_someNontrivialControlFlow) bailout(); 54 | })(); 55 | ``` 56 | 57 | Out: 58 | 59 | ```js 60 | foo(); 61 | bar.baz(); 62 | 63 | var x = 'x'; 64 | 65 | (function() { 66 | var someBinding; 67 | if (or_someNontrivialControlFlow) bailout(); 68 | })(); 69 | ``` 70 | 71 | ### Eliminate calls of identity functions 72 | 73 | In: 74 | 75 | ```js 76 | function id(x) { return x; } 77 | 78 | var foo = id(baz(), 1, '2'); 79 | 80 | var bar = id(baz(), someOtherImpureArgument()); 81 | ``` 82 | 83 | Out: 84 | 85 | ```js 86 | function id(x) { return x; } 87 | 88 | var foo = baz(); 89 | 90 | var bar = id(baz(), someOtherImpureArgument()); 91 | ``` 92 | 93 | #### `unsafe` option 94 | 95 | With `unsafe`, calls of identity functions whose arguments are impure due to unreferenced identifiers are eliminated. 96 | This is unlikely to be unsafe in practice, as you'd have to be relying on side-effects of a global getter or a thrown `ReferenceError`. 97 | 98 | In: 99 | 100 | ```js 101 | function toClass(val, class_) { return val; } 102 | 103 | var x = toClass(a, HTMLAnchorElement); 104 | ``` 105 | 106 | Out: 107 | 108 | ```js 109 | function toClass(val, class_) { return val; } 110 | 111 | var x = a; 112 | ``` 113 | 114 | ### Eliminate calls of `Object.freeze` and friends 115 | 116 | This is intended primarily to facilitate SROA (below). 117 | 118 | In: 119 | 120 | ```js 121 | var x = Object.freeze({ a: 5 }); 122 | ``` 123 | 124 | Out: 125 | 126 | ```js 127 | var x = { a: 5 }; 128 | ``` 129 | 130 | ### Scalar replacement of aggregates 131 | 132 | In: 133 | 134 | ```js 135 | var x = { 136 | a: { b: 5 }, 137 | c() {} 138 | }; 139 | ``` 140 | 141 | Out: 142 | 143 | ```js 144 | var _x$a$b = 5, 145 | _x$c = function c() {}; 146 | ``` 147 | 148 | #### `unsafe` option 149 | 150 | With `unsafe`, scalar replacement will be performed on objects whose properties are written to. 151 | This is unlikely to be unsafe in practice, as the assigned property (alone) would have to reference `this`. 152 | (This optimization always bails out if _existing_ object properties reference `this`.) 153 | 154 | In: 155 | 156 | ```js 157 | var x = { 158 | a: { b: 5 }, 159 | c() {} 160 | }; 161 | 162 | x.a.b *= 2; 163 | ``` 164 | 165 | Out: 166 | 167 | ```js 168 | var _x$a$b = 5, 169 | _x$c = function c() {}; 170 | 171 | _x$a$b *= 2; 172 | ``` 173 | 174 | ### Store-to-load forwarding / copy propagation 175 | 176 | In: 177 | 178 | ```js 179 | var a = () => {}; 180 | var _a = a; 181 | function foo() { 182 | var b = _a; 183 | function bar() { 184 | var c = b; 185 | c(); 186 | } 187 | } 188 | ``` 189 | 190 | Out: 191 | 192 | ```js 193 | var a = () => {}; 194 | function foo() { 195 | function bar() { 196 | a(); 197 | } 198 | } 199 | ``` 200 | -------------------------------------------------------------------------------- /src/plugins/scalar-replacement.js: -------------------------------------------------------------------------------- 1 | // Scalar replacement of aggregates 2 | 3 | module.exports = function scalarReplacementPlugin({ types: t }) { 4 | return { 5 | visitor: { 6 | VariableDeclarator(path) { 7 | const { unsafe } = this.opts; 8 | 9 | if (!t.isIdentifier(path.node.id)) return; // not a pure store (destructuring) 10 | if (!t.isObjectExpression(path.node.init)) return; // only immediate object literals can be SROA'd 11 | 12 | const binding = path.scope.getBinding(path.node.id.name); 13 | if (!binding.constant) return; // variable is mutated, bail out 14 | 15 | // ensure that `this` is not referenced from within the object, 16 | // as this could allow it to escape 17 | let referencedThis = false; 18 | path.get('init').traverse({ 19 | ThisExpression(innerPath) { 20 | referencedThis = true; 21 | innerPath.stop(); 22 | } 23 | }); 24 | if (referencedThis) return; 25 | 26 | // populate the map with initial values of each object property 27 | const properties = Object.create(null); // raw key -> value 28 | for (const prop of path.node.init.properties) { 29 | if (t.isSpreadProperty(prop)) return; // object spread 30 | 31 | let name; 32 | if (!prop.computed && t.isIdentifier(prop.key)) { 33 | name = prop.key.name; 34 | } else if (t.isStringLiteral(prop.key)) { 35 | name = prop.key.value; 36 | } else { 37 | return; // computed property 38 | } 39 | 40 | if (name === '__proto__') return; // magic __proto__ property 41 | 42 | let value; 43 | if (t.isObjectProperty(prop)) { 44 | value = prop.value; 45 | } else { 46 | t.assertObjectMethod(prop); // spread element handled above 47 | 48 | switch (prop.kind) { 49 | case 'method': 50 | value = t.functionExpression( 51 | // not quite the same name mangling as below, as leading numbers are replaced 52 | t.identifier(name.replace(/^\d|[^\w_$]/g, '_')), 53 | prop.params, 54 | prop.body, 55 | prop.generator, 56 | prop.async 57 | ); 58 | break; 59 | case 'get': 60 | case 'set': 61 | default: 62 | return; // getters and setters 63 | } 64 | } 65 | 66 | properties[name] = value; 67 | } 68 | 69 | // populate a map of references, and verify usages are legal (non-escaping, etc.) 70 | const propertyReferences = Object.create(null); // raw key -> [references] 71 | for (const refPath of binding.referencePaths) { 72 | if (!t.isMemberExpression(refPath.parent)) return; // object escapes through something other than property access 73 | 74 | // check for mutation 75 | if (t.isAssignmentExpression(refPath.parentPath.parent) || 76 | t.isArrayPattern(refPath.parentPath.parent) || 77 | t.isObjectProperty(refPath.parentPath.parent) && t.isObjectPattern(refPath.parentPath.parentPath.parent)) { 78 | if (!unsafe) return; // mutation 79 | } 80 | 81 | let name; 82 | if (!refPath.parent.computed && t.isIdentifier(refPath.parent.property)) { 83 | name = refPath.parent.property.name; 84 | } else if (t.isStringLiteral(refPath.parent.property)) { 85 | name = refPath.parent.property.value; 86 | } else { 87 | return; // computed member access 88 | } 89 | 90 | // if a property which isn't in the initial object is referenced, bail out 91 | if (!(name in properties)) return; 92 | 93 | propertyReferences[name] = (propertyReferences[name] || []).concat([refPath]); 94 | } 95 | 96 | // generate unique identifiers for each property, and emit them into the variable declaration 97 | const identifiers = Object.create(null); // raw key -> identifier 98 | for (const name in properties) { 99 | const val = properties[name]; 100 | 101 | // leading numbers are fine here because the variable already has a prefix 102 | const identifier = t.identifier(path.scope.generateUid(path.node.id.name + '$' + name.replace(/[^\w_$]/g, '_'))); 103 | 104 | // emit variable declaration into scope 105 | path.insertBefore(t.variableDeclarator(identifier, val)); 106 | 107 | identifiers[name] = identifier; 108 | } 109 | 110 | // replace each property reference with the corresponding unique identifier 111 | for (const name in propertyReferences) { 112 | for (const refPath of propertyReferences[name]) { 113 | refPath.parentPath.replaceWith(t.clone(identifiers[name])); 114 | } 115 | } 116 | 117 | // remove the old object and repair bindings 118 | path.remove(); 119 | path.scope.crawl(); // record the removal of the object, and the references of the new variables 120 | } 121 | } 122 | }; 123 | }; 124 | -------------------------------------------------------------------------------- /src/plugins/__tests__/inline-identity.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const thePlugin = require('../../../utils/test-transform')(require('../inline-identity')); 4 | const theUnsafePlugin = require('../../../utils/test-transform')([[require('../inline-identity'), { unsafe: true }]]); 5 | 6 | describe('inline-identity', () => { 7 | describe('succeeds on', () => { 8 | thePlugin('function declaration', ` 9 | function i(x) { 10 | return x; 11 | } 12 | var x = i(1); 13 | `, ` 14 | function i(x) { 15 | return x; 16 | } 17 | var x = 1; 18 | `); 19 | thePlugin('function expression', ` 20 | var i = function (x) { 21 | return x; 22 | }; 23 | var x = i(1); 24 | `, ` 25 | var i = function (x) { 26 | return x; 27 | }; 28 | var x = 1; 29 | `); 30 | thePlugin('arrow function expression, expression body', ` 31 | var i = x => x; 32 | var x = i(1); 33 | `, ` 34 | var i = x => x; 35 | var x = 1; 36 | `); 37 | thePlugin('arrow function expression, block body', ` 38 | var i = x => { 39 | return x; 40 | }; 41 | var x = i(1); 42 | `, ` 43 | var i = x => { 44 | return x; 45 | }; 46 | var x = 1; 47 | `); 48 | thePlugin('additional params', ` 49 | function i(x, y) { 50 | return x; 51 | } 52 | var x = i(1); 53 | `, ` 54 | function i(x, y) { 55 | return x; 56 | } 57 | var x = 1; 58 | `); 59 | thePlugin('additional pure arguments', ` 60 | function i(x) { 61 | return x; 62 | } 63 | var x = i(1, i, 42); 64 | `, ` 65 | function i(x) { 66 | return x; 67 | } 68 | var x = 1; 69 | `); 70 | thePlugin('impure first argument', ` 71 | function i(x) { 72 | return x; 73 | } 74 | var x = i(foo()); 75 | `, ` 76 | function i(x) { 77 | return x; 78 | } 79 | var x = foo(); 80 | `); 81 | thePlugin('no first argument', ` 82 | function i(x) { 83 | return x; 84 | } 85 | var x = i(); 86 | `, ` 87 | function i(x) { 88 | return x; 89 | } 90 | var x = void 0; 91 | `); 92 | thePlugin('dead code after return', ` 93 | function i(x) { 94 | return x; 95 | console.log(); 96 | } 97 | var x = i(1); 98 | `, ` 99 | function i(x) { 100 | return x; 101 | console.log(); 102 | } 103 | var x = 1; 104 | `); 105 | 106 | describe('with unsafe', () => { 107 | theUnsafePlugin('additional sort-of-pure arguments', ` 108 | function i(x) { 109 | return x; 110 | } 111 | var x = i(1, unreferencedGlobal); 112 | `, ` 113 | function i(x) { 114 | return x; 115 | } 116 | var x = 1; 117 | `); 118 | }); 119 | }); 120 | 121 | describe('bails out on', () => { 122 | thePlugin('impure arguments', ` 123 | function i(x) { 124 | return x; 125 | } 126 | var x = i(1, foo()); 127 | `); 128 | thePlugin('mutation of function declaration', ` 129 | function i(x) { 130 | return x; 131 | } 132 | var x = i(1); 133 | i = 5; 134 | `); 135 | thePlugin('mutation of variable', ` 136 | var i = function (x) { 137 | return x; 138 | }; 139 | var x = i(1); 140 | i = 5; 141 | `); 142 | thePlugin('variables not holding a function', ` 143 | var i = 5; 144 | var x = i(1); 145 | `); 146 | thePlugin('classes', ` 147 | class i {}; 148 | var x = i(1); 149 | `); 150 | thePlugin('unknown functions', ` 151 | var x = i(1); 152 | `); 153 | thePlugin('property access', ` 154 | function i(x) { 155 | return x; 156 | } 157 | var x = a.i(1); 158 | `); 159 | thePlugin('not a pure function', ` 160 | function i(x) { 161 | console.log(1); 162 | return x; 163 | } 164 | var x = a.i(1); 165 | `); 166 | thePlugin('not a pure function 2', ` 167 | function i(x) { 168 | foo; 169 | return x; 170 | } 171 | var x = a.i(1); 172 | `); 173 | thePlugin('conditional return', ` 174 | function i(x) { 175 | if (true) return x; 176 | } 177 | var x = a.i(1); 178 | `); 179 | thePlugin('arrow functions returning a different identifier', ` 180 | var i = x => y; 181 | var ii = () => y; 182 | i(1); 183 | ii(2); 184 | `); 185 | thePlugin('returning an expression', ` 186 | var i = x => ({ x }); 187 | var ii = function (x) { 188 | return { x }; 189 | }; 190 | i(1); 191 | ii(2); 192 | `); 193 | thePlugin('additional sort-of-pure arguments', ` 194 | function i(x) { 195 | return x; 196 | } 197 | var x = i(1, unreferencedGlobal); 198 | `); 199 | 200 | describe('with unsafe', () => { 201 | theUnsafePlugin('impure arguments after sort-of-pure arguments', ` 202 | function i(x) { 203 | return x; 204 | } 205 | var x = i(1, unreferencedGlobal, actuallyUnsafe()); 206 | `); 207 | }); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /src/plugins/__tests__/flatten-iife.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const thePlugin = require('../../../utils/test-transform')(require('../flatten-iife')); 4 | 5 | describe('flatten-iife', () => { 6 | describe('succeeds on', () => { 7 | thePlugin('functions', ` 8 | (function () { 9 | foo(); 10 | })(); 11 | `, ` 12 | foo(); 13 | `); 14 | thePlugin('arrows', ` 15 | (() => { 16 | foo(); 17 | })(); 18 | `, ` 19 | foo(); 20 | `); 21 | thePlugin('expression-body arrows', ` 22 | (() => foo())(); 23 | `, ` 24 | foo(); 25 | `); 26 | thePlugin('functions, no body', ` 27 | (function () {})(); 28 | `, ``); 29 | thePlugin('arrows, no body', ` 30 | (() => {})(); 31 | `, ``); 32 | thePlugin('multiple statements', ` 33 | (function () { 34 | foo(); 35 | bar(); 36 | })(); 37 | `, ` 38 | foo(); 39 | bar(); 40 | `); 41 | thePlugin('return, not in statement position', ` 42 | (function () { 43 | foo(); 44 | return bar(); 45 | })(); 46 | `, ` 47 | foo(); 48 | bar(); 49 | `); 50 | thePlugin('functions, in expression position', ` 51 | var x = (function () { 52 | return bar(); 53 | })(); 54 | `, ` 55 | var x = bar(); 56 | `); 57 | thePlugin('arrows, in expression position', ` 58 | var x = (() => { 59 | return bar(); 60 | })(); 61 | `, ` 62 | var x = bar(); 63 | `); 64 | thePlugin('expression-body arrows, in expression position', ` 65 | var x = (() => foo())(); 66 | `, ` 67 | var x = foo(); 68 | `); 69 | thePlugin('functions, in expression position, multiple statements', ` 70 | var x = (function () { 71 | foo(); 72 | return bar(); 73 | })(); 74 | `, ` 75 | var x = (foo(), bar()); 76 | `); 77 | thePlugin('arrows, in expression position, multiple statements', ` 78 | var x = (() => { 79 | foo(); 80 | return bar(); 81 | })(); 82 | `, ` 83 | var x = (foo(), bar()); 84 | `); 85 | thePlugin('functions, in expression position, no return', ` 86 | var x = (function () { 87 | foo(); 88 | bar(); 89 | })(); 90 | `, ` 91 | var x = (foo(), bar(), void 0); 92 | `); 93 | thePlugin('arrows, in expression position, no return', ` 94 | var x = (() => { 95 | foo(); 96 | bar(); 97 | })(); 98 | `, ` 99 | var x = (foo(), bar(), void 0); 100 | `); 101 | thePlugin('functions, in expression position, no body', ` 102 | var x = (function () {})(); 103 | `, ` 104 | var x = void 0; 105 | `); 106 | thePlugin('arrows, in expression position, no body', ` 107 | var x = (() => {})(); 108 | `, ` 109 | var x = void 0; 110 | `); 111 | thePlugin('multiple returns, not in statement position', ` 112 | (function () { 113 | x(); 114 | return foo(); 115 | y(); 116 | return bar(); 117 | })(); 118 | `, ` 119 | x(); 120 | foo(); 121 | `); 122 | thePlugin('multiple returns, in statement position', ` 123 | var z = (function () { 124 | x(); 125 | return foo(); 126 | y(); 127 | return bar(); 128 | })(); 129 | `, ` 130 | var z = (x(), foo()); 131 | `); 132 | }); 133 | 134 | describe('bails out on', () => { 135 | thePlugin('ordinary functions', ` 136 | function foo() {} 137 | var x = () => y; 138 | foo(); 139 | x(); 140 | (function () {}); 141 | () => {}; 142 | `); 143 | thePlugin('params', ` 144 | (function (x) {})(); 145 | `); 146 | thePlugin('arguments', ` 147 | (function () {})(1); 148 | `); 149 | thePlugin('var bindings', ` 150 | (function () { 151 | var x; 152 | })(); 153 | `); 154 | thePlugin('function bindings', ` 155 | (function () { 156 | function foo() {} 157 | })(); 158 | `); 159 | thePlugin('function bindings after return', ` 160 | (function () { 161 | return 1; 162 | function foo() {} 163 | })(); 164 | `); 165 | thePlugin('class bindings', ` 166 | (function () { 167 | class Foo {} 168 | })(); 169 | `); 170 | thePlugin('block scopes', ` 171 | (function () { 172 | {}; 173 | })(); 174 | `); 175 | thePlugin('non-expression statements', ` 176 | (function () { 177 | if (true) {} 178 | })(); 179 | `); 180 | thePlugin('throw', ` 181 | (function () { 182 | throw 1; 183 | })(); 184 | `); 185 | thePlugin('this', ` 186 | (function () { 187 | this; 188 | })(); 189 | `); 190 | thePlugin('this, inner', ` 191 | (function () { 192 | console.log(this); 193 | })(); 194 | `); 195 | thePlugin('arguments', ` 196 | (function () { 197 | arguments; 198 | })(); 199 | `); 200 | thePlugin('arguments, inner', ` 201 | (function () { 202 | console.log(arguments); 203 | })(); 204 | `); 205 | thePlugin('async functions', ` 206 | (async function () {})(); 207 | `); 208 | thePlugin('async arrows', ` 209 | (async () => {})(); 210 | `); 211 | thePlugin('generators', ` 212 | (function* () {})(); 213 | `); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /src/plugins/__tests__/store-to-load.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const thePlugin = require('../../../utils/test-transform')(require('../store-to-load')); 4 | const thePluginVerifies = require('../../../utils/test-transform').withVerifier(require('../store-to-load')); 5 | const dumpScope = require('../../../utils/dump-scope'); 6 | 7 | const declarations = ` 8 | var something = 1; 9 | class Foo {} 10 | function foo() {} 11 | `; 12 | 13 | describe('store-to-load', () => { 14 | describe('succeeds on', () => { 15 | thePlugin('basic', ` 16 | ${declarations} 17 | var x = something; 18 | var y = Foo; 19 | var z = foo; 20 | function main() { 21 | x; 22 | y; 23 | z; 24 | } 25 | `, ` 26 | ${declarations} 27 | function main() { 28 | something; 29 | Foo; 30 | foo; 31 | } 32 | `); 33 | thePlugin('multiple variables per declaration', ` 34 | ${declarations} 35 | var a = Foo, 36 | b = foo; 37 | function main1() { 38 | a; 39 | b; 40 | } 41 | `, ` 42 | ${declarations} 43 | function main1() { 44 | Foo; 45 | foo; 46 | } 47 | `); 48 | thePlugin('not removing other variables in declaration', ` 49 | ${declarations} 50 | var a = Foo, 51 | b = 5; 52 | function main1() { 53 | a; 54 | b; 55 | } 56 | `, ` 57 | ${declarations} 58 | var b = 5; 59 | function main1() { 60 | Foo; 61 | b; 62 | } 63 | `); 64 | thePlugin('forwarding through multiple variables', ` 65 | ${declarations} 66 | var c = something; 67 | var d = c; 68 | function main2() { 69 | c; 70 | d; 71 | } 72 | `, ` 73 | ${declarations} 74 | function main2() { 75 | something; 76 | something; 77 | } 78 | `); 79 | thePlugin('inner function scopes', ` 80 | ${declarations} 81 | var e = something; 82 | function main3() { 83 | e; 84 | function main3inner() { 85 | var xy = 1; 86 | e; 87 | } 88 | } 89 | `, ` 90 | ${declarations} 91 | function main3() { 92 | something; 93 | function main3inner() { 94 | var xy = 1; 95 | something; 96 | } 97 | } 98 | `); 99 | thePlugin('inner block scopes', ` 100 | ${declarations} 101 | var e = something; 102 | { 103 | e; 104 | } 105 | `, ` 106 | ${declarations} 107 | { 108 | something; 109 | } 110 | `); 111 | thePlugin('through variables in multiple scopes', ` 112 | ${declarations} 113 | var a = something; 114 | function x() { 115 | const b = a; 116 | { 117 | let c = b; 118 | { 119 | d; 120 | } 121 | } 122 | } 123 | `, ` 124 | ${declarations} 125 | function x() { 126 | { 127 | { 128 | d; 129 | } 130 | } 131 | } 132 | `); 133 | thePlugin('shadowed identifiers', ` 134 | var something = 1; 135 | class Foo {} 136 | function foo() {} 137 | 138 | var a = something; 139 | var b = Foo; 140 | var c = foo; 141 | function x() { 142 | var something = 5; 143 | var Foo = 6; 144 | var foo = 7; 145 | a; 146 | b; 147 | c; 148 | } 149 | `, ` 150 | var _something = 1; 151 | class _Foo {} 152 | function _foo() {} 153 | 154 | function x() { 155 | var something = 5; 156 | var Foo = 6; 157 | var foo = 7; 158 | _something; 159 | _Foo; 160 | _foo; 161 | } 162 | `); 163 | thePlugin('double-shadowed identifiers', ` 164 | var something = 1; 165 | class Foo {} 166 | function foo() {} 167 | 168 | var a = something; 169 | var b = Foo; 170 | var c = foo; 171 | function x() { 172 | var something = 5; 173 | var Foo = 6; 174 | var foo = 7; 175 | var _something = 8; 176 | var _Foo = 9; 177 | var _foo = 0; 178 | a; 179 | b; 180 | c; 181 | } 182 | `, ` 183 | var _something2 = 1; 184 | class _Foo2 {} 185 | function _foo2() {} 186 | 187 | function x() { 188 | var something = 5; 189 | var Foo = 6; 190 | var foo = 7; 191 | var _something = 8; 192 | var _Foo = 9; 193 | var _foo = 0; 194 | _something2; 195 | _Foo2; 196 | _foo2; 197 | } 198 | `); 199 | thePlugin('triple-shadowed identifiers', ` 200 | var something = 1; 201 | 202 | var a = something; 203 | function x() { 204 | var something = 5; 205 | var _something = 8; 206 | var _something2 = 11; 207 | a; 208 | } 209 | `, ` 210 | var _something3 = 1; 211 | 212 | function x() { 213 | var something = 5; 214 | var _something = 8; 215 | var _something2 = 11; 216 | _something3; 217 | } 218 | `); 219 | thePlugin('double-shadowed identifiers, non-forwarded references', ` 220 | var something = 1; 221 | class Foo {} 222 | function foo() {} 223 | 224 | var a = something; 225 | var b = Foo; 226 | var c = foo; 227 | function x() { 228 | var something = 5; 229 | var Foo = 6; 230 | var foo = 7; 231 | a; 232 | b; 233 | c; 234 | } 235 | function y() { 236 | var _something = 8; 237 | var _Foo = 9; 238 | var _foo = 0; 239 | something; 240 | Foo; 241 | foo; 242 | } 243 | `, ` 244 | var _something2 = 1; 245 | class _Foo2 {} 246 | function _foo2() {} 247 | 248 | function x() { 249 | var something = 5; 250 | var Foo = 6; 251 | var foo = 7; 252 | _something2; 253 | _Foo2; 254 | _foo2; 255 | } 256 | function y() { 257 | var _something = 8; 258 | var _Foo = 9; 259 | var _foo = 0; 260 | _something2; 261 | _Foo2; 262 | _foo2; 263 | } 264 | `); 265 | thePlugin('triple-shadowed identifiers, non-forwarded references', ` 266 | var something = 1; 267 | 268 | var a = something; 269 | function x() { 270 | var something = 5; 271 | a; 272 | } 273 | function y() { 274 | var _something = 8; 275 | var _something2 = 11; 276 | something; 277 | } 278 | `, ` 279 | var _something3 = 1; 280 | 281 | function x() { 282 | var something = 5; 283 | _something3; 284 | } 285 | function y() { 286 | var _something = 8; 287 | var _something2 = 11; 288 | _something3; 289 | } 290 | `); 291 | thePluginVerifies('correctly fixes bindings', path => { 292 | const fooScope = path.get('body.1.body.body.0').scope; 293 | expect(dumpScope(fooScope)).toBe([ 294 | 'FunctionDeclaration', 295 | `- something ${JSON.stringify({ constant: true, references: 0, violations: 0, kind: 'var' })}`, 296 | 'FunctionDeclaration', 297 | `- foo ${JSON.stringify({ constant: true, references: 0, violations: 0, kind: 'hoisted' })}`, 298 | `- bar ${JSON.stringify({ constant: true, references: 0, violations: 0, kind: 'hoisted' })}`, 299 | 'Program', 300 | `- _something ${JSON.stringify({ constant: true, references: 2, violations: 0, kind: 'var' })}`, 301 | `- main ${JSON.stringify({ constant: true, references: 0, violations: 0, kind: 'hoisted' })}`, 302 | ].join('\n')); 303 | }, ` 304 | var something = 1; 305 | function main() { 306 | var x = something; 307 | function foo() { 308 | var something = 5; 309 | x; 310 | } 311 | function bar() { 312 | something; 313 | } 314 | } 315 | `, ` 316 | var _something = 1; 317 | function main() { 318 | function foo() { 319 | var something = 5; 320 | _something; 321 | } 322 | function bar() { 323 | _something; 324 | } 325 | } 326 | `); 327 | }); 328 | 329 | describe('bails out on', () => { 330 | thePlugin('destructuring', ` 331 | ${declarations} 332 | var { call } = something; 333 | call; 334 | `); 335 | thePlugin('stored to mutated variable', ` 336 | ${declarations} 337 | var a = something; 338 | a; 339 | a = foo; 340 | `); 341 | thePlugin('stored value of mutated variable', ` 342 | function something() {} 343 | var a = something; 344 | a; 345 | something = somethingElse; 346 | `); 347 | thePlugin('unreferenced identifier', ` 348 | var a = something; 349 | a; 350 | `); 351 | }); 352 | }); 353 | -------------------------------------------------------------------------------- /src/plugins/__tests__/scalar-replacement.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | const thePlugin = require('../../../utils/test-transform')(require('../scalar-replacement')); 4 | const theUnsafePlugin = require('../../../utils/test-transform')([[require('../scalar-replacement'), { unsafe: true }]]); 5 | const thePluginVerifies = require('../../../utils/test-transform').withVerifier(require('../scalar-replacement')); 6 | const dumpScope = require('../../../utils/dump-scope'); 7 | 8 | describe('scalar-replacement', () => { 9 | describe('succeeds on', () => { 10 | thePlugin('basic', ` 11 | var x = { 12 | a: 1, 13 | }; 14 | `, ` 15 | var _x$a = 1; 16 | `); 17 | thePlugin('nested', ` 18 | var x = { 19 | a: 1, 20 | b: { c: 2 } 21 | }; 22 | `, ` 23 | var _x$a = 1, 24 | _x$b$c = 2; 25 | `); 26 | thePlugin('replacing references', ` 27 | var x = { 28 | a: 1 29 | }; 30 | x.a; 31 | `, ` 32 | var _x$a = 1; 33 | _x$a; 34 | `); 35 | thePlugin('replacing nested references', ` 36 | var x = { 37 | a: 1, 38 | b: { c: 5 } 39 | }; 40 | x.a; 41 | x.b.c; 42 | `, ` 43 | var _x$a = 1, 44 | _x$b$c = 5; 45 | _x$a; 46 | _x$b$c; 47 | `); 48 | thePlugin('preserves const and let', ` 49 | const x = { 50 | a: 1, 51 | }; 52 | let y = { 53 | b: 2, 54 | }; 55 | `, ` 56 | const _x$a = 1; 57 | let _y$b = 2; 58 | `); 59 | thePlugin('quoted props', ` 60 | var x = { 61 | 'a': 1, 62 | ['b']: 2 63 | }; 64 | `, ` 65 | var _x$a = 1, 66 | _x$b = 2; 67 | `); 68 | thePlugin('quoted member access', ` 69 | var x = { 70 | a: 1, 71 | }; 72 | x['a']; 73 | `, ` 74 | var _x$a = 1; 75 | _x$a; 76 | `); 77 | thePlugin('non-identifier props', ` 78 | var x = { 79 | '-': 1, 80 | }; 81 | x['-']; 82 | `, ` 83 | var _x$_ = 1; 84 | _x$_; 85 | `); 86 | thePlugin('semi-identifier props with numbers', ` 87 | var x = { 88 | '9n': 1 89 | }; 90 | x['9n']; 91 | `, ` 92 | var _x$9n = 1; 93 | _x$9n; 94 | `); 95 | thePlugin('non-identifier props name collisions', ` 96 | var x = { 97 | '-': 1, 98 | _: 2, 99 | '/': 3 100 | }; 101 | x['-']; 102 | x._; 103 | x['/']; 104 | `, ` 105 | var _x$_ = 1, 106 | _x$_2 = 2, 107 | _x$_3 = 3; 108 | _x$_; 109 | _x$_2; 110 | _x$_3; 111 | `); 112 | thePlugin('shadowed references', ` 113 | var x = { 114 | a: 1 115 | }; 116 | function foo() { 117 | var _x$a; 118 | x.a; 119 | } 120 | `, ` 121 | var _x$a2 = 1; 122 | function foo() { 123 | var _x$a; 124 | _x$a2; 125 | } 126 | `); 127 | thePlugin('externally-shadowed references', ` 128 | var x = { 129 | a: 1 130 | }; 131 | var _x$a; 132 | function foo() { 133 | x.a; 134 | } 135 | `, ` 136 | var _x$a2 = 1; 137 | var _x$a; 138 | function foo() { 139 | _x$a2; 140 | } 141 | `); 142 | thePlugin('double-shadowed references', ` 143 | var x = { 144 | a: 1 145 | }; 146 | function foo() { 147 | var _x$a; 148 | var _x$a2; 149 | x.a; 150 | } 151 | `, ` 152 | var _x$a3 = 1; 153 | function foo() { 154 | var _x$a; 155 | var _x$a2; 156 | _x$a3; 157 | } 158 | `); 159 | thePlugin('double-externally-shadowed references', ` 160 | var x = { 161 | a: 1 162 | }; 163 | var _x$a; 164 | var _x$a2; 165 | function foo() { 166 | x.a; 167 | } 168 | `, ` 169 | var _x$a3 = 1; 170 | var _x$a; 171 | var _x$a2; 172 | function foo() { 173 | _x$a3; 174 | } 175 | `); 176 | thePlugin('combination-shadowed references', ` 177 | var x = { 178 | a: 1 179 | }; 180 | var _x$a; 181 | function foo() { 182 | var _x$a2; 183 | x.a; 184 | } 185 | `, ` 186 | var _x$a3 = 1; 187 | var _x$a; 188 | function foo() { 189 | var _x$a2; 190 | _x$a3; 191 | } 192 | `); 193 | thePlugin('duplicate keys', ` 194 | var x = { 195 | a: 1, 196 | a: 2, 197 | ['a']: 3, 198 | }; 199 | `, ` 200 | var _x$a = 3; 201 | `); 202 | thePlugin('arrow functions', ` 203 | var x = { 204 | a: () => 1 205 | }; 206 | `, ` 207 | var _x$a = () => 1; 208 | `); 209 | thePlugin('methods', ` 210 | var x = { 211 | a(y) { 212 | y; 213 | }, 214 | async b(z) { 215 | z; 216 | }, 217 | *c(w) { 218 | w; 219 | } 220 | }; 221 | `, ` 222 | var _x$a = function a(y) { 223 | y; 224 | }, 225 | _x$b = async function b(z) { 226 | z; 227 | }, 228 | _x$c = function* c(w) { 229 | w; 230 | }; 231 | `); 232 | thePlugin('methods with string names', ` 233 | var x = { 234 | 'a'(y) { 235 | y; 236 | }, 237 | async 'b'(z) { 238 | z; 239 | }, 240 | *'c'(w) { 241 | w; 242 | } 243 | }; 244 | `, ` 245 | var _x$a = function a(y) { 246 | y; 247 | }, 248 | _x$b = async function b(z) { 249 | z; 250 | }, 251 | _x$c = function* c(w) { 252 | w; 253 | }; 254 | `); 255 | thePlugin('methods with computed names', ` 256 | var x = { 257 | ['-a'](y) { 258 | y; 259 | }, 260 | async ['9b'](z) { 261 | z; 262 | }, 263 | *['-c'](w) { 264 | w; 265 | } 266 | }; 267 | `, ` 268 | var _x$_a = function _a(y) { 269 | y; 270 | }, 271 | _x$9b = async function _b(z) { 272 | z; 273 | }, 274 | _x$_c = function* _c(w) { 275 | w; 276 | }; 277 | `); 278 | thePlugin('preserves ordering of nearby vars', ` 279 | var x = 1, 280 | y = { 281 | a: 2, 282 | b: 3, 283 | }, 284 | z = 4; 285 | `, ` 286 | var x = 1, 287 | _y$a = 2, 288 | _y$b = 3, 289 | z = 4; 290 | `); 291 | thePlugin('preserves ordering when nested', ` 292 | var x = { 293 | a: 1, 294 | b: { 295 | c: 3, 296 | d: 4, 297 | }, 298 | e: 5, 299 | }; 300 | `, ` 301 | var _x$a = 1, 302 | _x$b$c = 3, 303 | _x$b$d = 4, 304 | _x$e = 5; 305 | `); 306 | thePlugin('nearby `this`', ` 307 | var x = this, 308 | y = { 309 | a: 5 310 | }, 311 | z = this; 312 | this; 313 | `, ` 314 | var x = this, 315 | _y$a = 5, 316 | z = this; 317 | this; 318 | `); 319 | 320 | thePluginVerifies('correctly fixes bindings', path => { 321 | const fooScope = path.get('body.1').scope; 322 | expect(dumpScope(fooScope)).toBe([ 323 | 'FunctionDeclaration', 324 | 'Program', 325 | `- _x$a ${JSON.stringify({ constant: true, references: 1, violations: 0, kind: 'var' })}`, 326 | `- _x$b$c ${JSON.stringify({ constant: true, references: 1, violations: 0, kind: 'var' })}`, 327 | `- foo ${JSON.stringify({ constant: true, references: 0, violations: 0, kind: 'hoisted' })}`, 328 | ].join('\n')); 329 | }, ` 330 | var x = { 331 | a: 1, 332 | b: { c: 5 } 333 | }; 334 | function foo() { 335 | x.a; 336 | x.b.c; 337 | } 338 | `, ` 339 | var _x$a = 1, 340 | _x$b$c = 5; 341 | function foo() { 342 | _x$a; 343 | _x$b$c; 344 | } 345 | `); 346 | thePluginVerifies('doesn\'t break bindings when inserting into var sequence', path => { 347 | const fooScope = path.get('body.1').scope; 348 | expect(dumpScope(fooScope)).toBe([ 349 | 'FunctionDeclaration', 350 | 'Program', 351 | `- y ${JSON.stringify({ constant: false, references: 0, violations: 1, kind: 'var' })}`, 352 | `- _x$a ${JSON.stringify({ constant: true, references: 1, violations: 0, kind: 'var' })}`, 353 | `- _x$b$c ${JSON.stringify({ constant: true, references: 1, violations: 0, kind: 'var' })}`, 354 | `- z ${JSON.stringify({ constant: true, references: 1, violations: 0, kind: 'var' })}`, 355 | `- foo ${JSON.stringify({ constant: true, references: 0, violations: 0, kind: 'hoisted' })}`, 356 | ].join('\n')); 357 | }, ` 358 | var y = 1, 359 | x = { 360 | a: 1, 361 | b: { c: 5 } 362 | }, 363 | z = 2; 364 | function foo() { 365 | y = 2; 366 | x.a; 367 | x.b.c; 368 | z; 369 | } 370 | `, ` 371 | var y = 1, 372 | _x$a = 1, 373 | _x$b$c = 5, 374 | z = 2; 375 | function foo() { 376 | y = 2; 377 | _x$a; 378 | _x$b$c; 379 | z; 380 | } 381 | `); 382 | thePlugin('mutation only of inner object', ` 383 | var x = { 384 | a: { b: 5 } 385 | }; 386 | x.a.b = 5; 387 | `, ` 388 | var _x$a = { b: 5 }; 389 | _x$a.b = 5; 390 | `); 391 | 392 | describe('with unsafe', () => { 393 | theUnsafePlugin('mutation, assignment', ` 394 | var x = { 395 | a: 1 396 | }; 397 | x.a = 2; 398 | `, ` 399 | var _x$a = 1; 400 | _x$a = 2; 401 | `); 402 | theUnsafePlugin('mutation, exotic assignment', ` 403 | var x = { 404 | a: 1 405 | }; 406 | x.a += 1; 407 | var y = { 408 | a: 1 409 | }; 410 | y.a |= 2; 411 | `, ` 412 | var _x$a = 1; 413 | _x$a += 1; 414 | var _y$a = 1; 415 | _y$a |= 2; 416 | `); 417 | theUnsafePlugin('mutation, array destructuring', ` 418 | var x = { 419 | a: 1 420 | }; 421 | [x.a] = foo; 422 | `, ` 423 | var _x$a = 1; 424 | [_x$a] = foo; 425 | `); 426 | theUnsafePlugin('mutation, object destructuring', ` 427 | var x = { 428 | a: 1 429 | }; 430 | ({ y: x.a } = foo); 431 | `, ` 432 | var _x$a = 1; 433 | ({ y: _x$a } = foo); 434 | `); 435 | }); 436 | }); 437 | 438 | describe('bails out on', () => { 439 | thePlugin('mutated variable', ` 440 | var x = { 441 | a: 5 442 | }; 443 | x = {}; 444 | `); 445 | thePlugin('object escapes', ` 446 | var x = { 447 | a: 5 448 | }; 449 | x; 450 | `); 451 | thePlugin('object escapes through call', ` 452 | var x = { 453 | a: 5 454 | }; 455 | foo(x); 456 | `); 457 | thePlugin('object escapes through assignment', ` 458 | var x = { 459 | a: 5 460 | }; 461 | var y = x; 462 | `); 463 | thePlugin('getters', ` 464 | var x = { 465 | a: 5, 466 | get y() {} 467 | }; 468 | `); 469 | thePlugin('setters', ` 470 | var x = { 471 | a: 5, 472 | set y(v) {} 473 | }; 474 | `); 475 | thePlugin('__proto__', ` 476 | var x = { 477 | a: 5, 478 | __proto__: 6 479 | }; 480 | var y = { 481 | a: 5, 482 | '__proto__': 6 483 | }; 484 | `); 485 | thePlugin('unknown props', ` 486 | var x = { 487 | a: 5 488 | }; 489 | x.hasOwnProperty; 490 | `); 491 | thePlugin('computed props', ` 492 | var x = { 493 | ['x' + 'y']: 5 494 | }; 495 | `); 496 | thePlugin('computed props through variable', ` 497 | var x = { 498 | [y]: 5 499 | }; 500 | `); 501 | thePlugin('computed member access', ` 502 | var x = { 503 | ab: 5 504 | }; 505 | x['a' + 'b']; 506 | `); 507 | thePlugin('object spread', ` 508 | var x = { 509 | ab: 5, 510 | ...foo 511 | }; 512 | `); 513 | thePlugin('destructuring', ` 514 | var { a } = { 515 | a: 5 516 | }; 517 | `); 518 | thePlugin('computed member access through variable', ` 519 | var x = { 520 | ab: 5 521 | }; 522 | x[ab]; 523 | `); 524 | thePlugin('referencing this, simple cases', ` 525 | var x = { 526 | a: function () { 527 | this; 528 | } 529 | }; 530 | var y = { 531 | a() { 532 | this; 533 | } 534 | }; 535 | var z = { 536 | async a() { 537 | this; 538 | } 539 | }; 540 | var w = { 541 | *a() { 542 | this; 543 | } 544 | }; 545 | `); 546 | thePlugin('referencing this, in IIFE initializer', ` 547 | var x = { 548 | x: (() => { 549 | return function () { 550 | this; 551 | }; 552 | })() 553 | }; 554 | `); 555 | thePlugin('referencing this, deep in IIFE initializer', ` 556 | var x = { 557 | x: (() => { 558 | return function () { 559 | () => this; 560 | }; 561 | })() 562 | }; 563 | `); 564 | thePlugin('mutation, assignment', ` 565 | var x = { 566 | a: 1 567 | }; 568 | x.a = 2; 569 | `); 570 | thePlugin('mutation, exotic assignment', ` 571 | var x = { 572 | a: 1 573 | }; 574 | x.a += 1; 575 | var y = { 576 | a: 1 577 | }; 578 | y.a |= 2; 579 | `); 580 | thePlugin('mutation, array destructuring', ` 581 | var x = { 582 | a: 1 583 | }; 584 | [x.a] = foo; 585 | `); 586 | thePlugin('mutation, object destructuring', ` 587 | var x = { 588 | a: 1 589 | }; 590 | ({ y: x.a } = foo); 591 | `); 592 | thePlugin('mutation, complex destructuring', ` 593 | var x = { 594 | a: 1 595 | }; 596 | [{ foo: [{ y: x.a }] }] = foo; 597 | `); 598 | }); 599 | }); 600 | --------------------------------------------------------------------------------