├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── benchmarks ├── react-render.js └── simple.js ├── circle.yml ├── decls └── babel-misc.js ├── package.json ├── scripts ├── benchmark.js ├── download-flow-libs.js └── generate-fixtures.js ├── src └── index.js └── test ├── fixtures-babel-config.js ├── fixtures ├── handle-naming-edge-cases │ ├── actual.js │ └── expected.js ├── handle-references-to-this │ ├── actual.js │ └── expected.js ├── hoist-async-functions │ ├── actual.js │ └── expected.js ├── hoist-functions │ ├── actual.js │ └── expected.js ├── hoist-generator-functions │ ├── actual.js │ └── expected.js ├── hoist-nested-methods-if-options.methods-true │ ├── actual.js │ ├── expected.js │ └── options.json ├── hoist-out-of-loops │ ├── actual.js │ └── expected.js ├── not-hoist-mutated-funcs │ ├── actual.js │ ├── expected.js │ └── options.json ├── not-hoist-nested-methods-by-default │ ├── actual.js │ ├── expected.js │ └── options.json ├── not-hoist-nested-methods-if-options.methods-false │ ├── actual.js │ ├── expected.js │ └── options.json ├── react-render-callback │ ├── actual.js │ ├── expected.js │ └── options.json └── regression-classes-unbound-name │ ├── actual.js │ ├── expected.js │ └── options.json ├── mocha.opts └── specs ├── .eslintrc └── transform-fixtures.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-node4"], 3 | "plugins": ["syntax-flow", "syntax-class-properties", "transform-class-properties", "transform-flow-strip-types", "transform-async-to-generator"], 4 | "env": { 5 | "development": { 6 | "sourceMaps": true 7 | }, 8 | "test": { 9 | "sourceMaps": "both", 10 | "plugins": ["istanbul"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | decls/babel 3 | benchmarks/**/*.transformed.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["semistandard"], 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "babel", 6 | "flowtype" 7 | ], 8 | "rules": { 9 | "babel/new-cap": [2, { "newIsCap": true, "capIsNew": false }], 10 | "new-cap": 0, 11 | "no-var": "error", 12 | "prefer-const": "error", 13 | "no-console": "error", 14 | "flowtype/boolean-style": [ 15 | 2, 16 | "boolean" 17 | ], 18 | "flowtype/define-flow-type": 1, 19 | "flowtype/delimiter-dangle": [ 20 | 2, 21 | "never" 22 | ], 23 | "flowtype/generic-spacing": [ 24 | 2, 25 | "never" 26 | ], 27 | "flowtype/require-parameter-type": [ 28 | 1, 29 | { 30 | "excludeArrowFunctions": true 31 | } 32 | ], 33 | "flowtype/require-return-type": [ 34 | 1, 35 | "always", 36 | { 37 | "annotateUndefined": "never", 38 | "excludeArrowFunctions": true 39 | } 40 | ], 41 | "flowtype/space-after-type-colon": [ 42 | 1, 43 | "always" 44 | ], 45 | "flowtype/space-before-generic-bracket": [ 46 | 2, 47 | "never" 48 | ], 49 | "flowtype/space-before-type-colon": [ 50 | 1, 51 | "never" 52 | ], 53 | "flowtype/type-id-match": 0, 54 | "flowtype/union-intersection-spacing": [ 55 | 2, 56 | "always" 57 | ], 58 | "flowtype/use-flow-type": 1, 59 | "flowtype/valid-syntax": 1 60 | }, 61 | "settings": { 62 | "flowtype": { 63 | "onlyFilesWithFlowAnnotation": true 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /data/.* 3 | /lib/.* 4 | /test/artifacts/.* 5 | /test/benchmarks/.* 6 | /test/example/.* 7 | /test/fixtures/.* 8 | .*/node_modules/.*eslint.* 9 | .*/node_modules/babel-cli.* 10 | .*/node_modules/babel-helper-.* 11 | .*/node_modules/babel-plugin-.* 12 | .*/node_modules/cross-env/.* 13 | .*/node_modules/mocha.* 14 | .*/node_modules/npm-run-all/.* 15 | .*/node_modules/jsonlint/.* 16 | .*/node_modules/fbjs/.* 17 | .*/node_modules/config-chain/.* 18 | .*/node_modules/npmconf/.* 19 | 20 | [options] 21 | suppress_comment= \\(.\\|\n\\)*\\$Flow\\(FixMe\\|Issue\\) 22 | suppress_type=$FlowIssue 23 | 24 | [libs] 25 | decls/ 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | benchmarks/**/*.transformed.js 5 | decls/babel 6 | .nyc_output 7 | .vscode/GitHubIssue 8 | coverage 9 | test-results.xml 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.validate.enable": false, 4 | "javascript.validate.enable": false, 5 | "files.exclude": { 6 | "**/.git": true, 7 | "**/.svn": true, 8 | "**/.DS_Store": true, 9 | "lib": true, 10 | ".nyc_output": true, 11 | "node_modules": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-transform-hoist-nested-functions 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/motiz88/babel-plugin-transform-hoist-nested-functions.svg)](https://greenkeeper.io/) 4 | [![circle][circle-image]][circle-url] 5 | [![npm][npm-image]][npm-url] 6 | [![coverage][coverage-image]][coverage-url] 7 | 8 | [![semantic release][semantic-release-image]][semantic-release-url] 9 | [![js-semistandard-style][semistandard-image]][semistandard-url] 10 | [![MIT License][license-image]][license-url] 11 | 12 | Babel plugin to hoist nested functions to the outermost scope possible without changing their 13 | contract. 14 | 15 | ## Examples 16 | 17 | ### Example 1 - basic hoisting 18 | 19 | **In** 20 | 21 | ```js 22 | function renderApp () { 23 | return renderStateContainer( 24 | ({value}) => renderValue(value) 25 | ); 26 | } 27 | ``` 28 | 29 | **Out** 30 | 31 | ```js 32 | var _hoistedAnonymousFunc = ({ value }) => renderValue(value); 33 | 34 | function renderApp () { 35 | return renderStateContainer(_hoistedAnonymousFunc); 36 | } 37 | ``` 38 | 39 | ### Example 2 - nested method hoisting 40 | 41 | To enable this transformation, pass the `methods: true` option to the plugin (see below). 42 | The output code depends on the ES2015 `Symbol` feature and the stage 2 class properties proposal. 43 | You will _most likely_ want to run `babel-plugin-transform-class-properties` after `transform-hoist-nested-function`. 44 | 45 | **In** 46 | 47 | ```js 48 | class Foo { 49 | bar () { 50 | return () => this; 51 | } 52 | } 53 | ``` 54 | 55 | **Out** 56 | 57 | ```js 58 | const _hoistedMethod = new Symbol("_hoistedMethod"), 59 | 60 | class Foo { 61 | [_hoistedMethod] = () => this; 62 | 63 | bar() { 64 | return this[_hoistedMethod]; 65 | } 66 | } 67 | ``` 68 | 69 | ## Motivation 70 | 71 | Patterns like [React "render callbacks"](https://discuss.reactjs.org/t/children-as-a-function-render-callbacks/626), 72 | that make heavy use of nested functions, incur the nonzero runtime cost of creating those 73 | functions over and over. JavaScript engines [don't always optimize this cost away](https://bugs.chromium.org/p/v8/issues/detail?id=505). 74 | 75 | To mitigate this cost, this plugin moves functions out of inner scopes wherever possible. A 76 | function can be moved up through any scope that it does not reference explicitly. This is somewhat 77 | analogous to what [babel-plugin-transform-react-constant-elements](https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-constant-elements/) 78 | does (and in fact some of the same Babel machinery is applied). 79 | 80 | ## Caveats 81 | 82 | ### Experimental 83 | 84 | This is a new, experimental plugin. Expect changes (adhering religiously to semver), and 85 | please, please, **PLEASE** test and benchmark your code _very thoroughly_ before using this in 86 | anything important. 87 | 88 | ### Not 100% transparent 89 | 90 | While the plugin aims not to change the behavior of hoisted functions, the fact that they are 91 | reused rather than recreated does have some visible consequences. 92 | 93 | Consider the following code: 94 | 95 | ```js 96 | function factory () { 97 | return function foo () {}; // foo() will be hoisted right above factory() 98 | } 99 | factory() === factory(); // ⬅ value depends on whether foo() is hoisted 100 | ``` 101 | 102 | That last expression evaluates to `false` in plain JavaScript, but is `true` if `foo()` has been 103 | hoisted. 104 | 105 | More fundamentally, **references to hoisted inner functions are allowed to escape their enclosing 106 | scopes**. You should determine whether this is appropriate for your code before using this plugin. 107 | 108 | ## Benchmarks 109 | 110 | [Here][benchmark-url] are benchmark results from the latest successful build on `master` using Node 111 | v4 (make your own with `npm run benchmark`). The benchmark code is [here][benchmarks-directory] - 112 | each file exports a single function that is repeatedly run and timed by [Benchmark.js] 113 | (https://benchmarkjs.com). 114 | 115 | From these preliminary results, it appears that hoisting functions this way can in fact improve 116 | performance, at least in principle; but the benefit may not always be significant. 117 | 118 | ## Installation 119 | 120 | ```sh 121 | $ npm install --save-dev babel-plugin-transform-hoist-nested-functions 122 | ``` 123 | 124 | ## Usage 125 | 126 | ### Via `.babelrc` (Recommended) 127 | 128 | **.babelrc** 129 | 130 | ```js 131 | // without options 132 | { 133 | "plugins": ["transform-hoist-nested-functions"] 134 | } 135 | 136 | // with options 137 | // NOTE: transform-class-properties is required in order to run the code 138 | { 139 | "plugins": [ 140 | ["transform-hoist-nested-functions", { 141 | "methods": true 142 | }], 143 | "transform-class-properties" 144 | ] 145 | } 146 | ``` 147 | 148 | ### Via CLI 149 | 150 | ```sh 151 | $ babel --plugins transform-hoist-nested-functions script.js 152 | ``` 153 | 154 | ### Via Node API 155 | 156 | ```javascript 157 | require("babel-core").transform("code", { 158 | plugins: ["transform-hoist-nested-functions"] 159 | }); 160 | ``` 161 | 162 | ## Development 163 | 164 | Use npm v3: `npm install -g npm@3` 165 | 166 | ```sh 167 | git clone https://github.com/motiz88/babel-plugin-transform-hoist-nested-functions 168 | cd babel-plugin-transform-hoist-nested-functions 169 | npm install 170 | # ... hackity hack hack ... 171 | npm run test:local # Including tests (mocha), code coverage (nyc), code style (eslint), type checks 172 | # (flow) and benchmarks. 173 | ``` 174 | 175 | See package.json for more dev scripts you can use. 176 | 177 | ## Contributing 178 | 179 | PRs are very welcome. Please make sure that `test:local` passes on your branch. 180 | 181 | [circle-image]: https://img.shields.io/circleci/project/motiz88/babel-plugin-transform-hoist-nested-functions/master.svg?style=flat-square 182 | [circle-url]: https://circleci.com/gh/motiz88/babel-plugin-transform-hoist-nested-functions 183 | [npm-image]: https://img.shields.io/npm/v/babel-plugin-transform-hoist-nested-functions.svg?style=flat-square 184 | [npm-url]: https://npmjs.org/package/babel-plugin-transform-hoist-nested-functions 185 | [semantic-release-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square 186 | [semantic-release-url]: https://github.com/semantic-release/semantic-release 187 | [license-image]: http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 188 | [license-url]: http://motiz88.mit-license.org/ 189 | [semistandard-image]: https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square 190 | [semistandard-url]: https://github.com/Flet/semistandard 191 | [coverage-image]: https://img.shields.io/codecov/c/github/motiz88/babel-plugin-transform-hoist-nested-functions.svg 192 | [coverage-url]: https://codecov.io/gh/motiz88/babel-plugin-transform-hoist-nested-functions 193 | [benchmark-url]: https://circleci.com/api/v1/project/motiz88/babel-plugin-transform-hoist-nested-functions/latest/artifacts/0/$CIRCLE_ARTIFACTS/benchmark.log?filter=successful&branch=master 194 | [benchmarks-directory]: https://github.com/motiz88/babel-plugin-transform-hoist-nested-functions/tree/master/benchmarks 195 | -------------------------------------------------------------------------------- /benchmarks/react-render.js: -------------------------------------------------------------------------------- 1 | // jsdom setup - used with enzyme full rendering 2 | 3 | const jsdom = require('jsdom').jsdom; 4 | 5 | global.document = jsdom(''); 6 | global.window = document.defaultView; 7 | Object.keys(document.defaultView).forEach((property) => { 8 | if (typeof global[property] === 'undefined') { 9 | global[property] = document.defaultView[property]; 10 | } 11 | }); 12 | 13 | global.navigator = { 14 | userAgent: 'node.js' 15 | }; 16 | 17 | const {createElement} = require('react'); 18 | const { mount } = require('enzyme'); 19 | 20 | function CounterState ({children, counter}) { 21 | return children({counter}); 22 | } 23 | 24 | function App (props) { 25 | return createElement( 26 | 'ol', 27 | null, 28 | createElement( 29 | CounterState, 30 | props, 31 | ({counter}) => createElement('li', null, 'Counter 1 is ', counter) 32 | ), 33 | createElement( 34 | CounterState, 35 | props, 36 | ({counter}) => createElement('li', null, 'Counter 2 is ', counter) 37 | ) 38 | ); 39 | } 40 | 41 | let counter = 0; 42 | const wrapper = mount(createElement(App, {counter})); 43 | module.exports = function () { 44 | wrapper.setProps({ counter: ++counter }); 45 | }; 46 | -------------------------------------------------------------------------------- /benchmarks/simple.js: -------------------------------------------------------------------------------- 1 | let x; 2 | 3 | module.exports = function () { 4 | trampoline(function inner (param) { 5 | x = 2 * param; 6 | }); 7 | return x; 8 | }; 9 | 10 | function trampoline (fn) { 11 | fn(Math.random()); 12 | } 13 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4 4 | environment: 5 | MOCHA_FILE: $CIRCLE_TEST_REPORTS/test-results.xml 6 | ESLINT_FILE: $CIRCLE_TEST_REPORTS/eslint-results.xml 7 | dependencies: 8 | pre: 9 | - npm install -g npm@3 10 | test: 11 | post: 12 | - cp benchmark.log $CIRCLE_ARTIFACTS/ 13 | - mkdir -p $CIRCLE_ARTIFACTS/benchmarks 14 | - cp benchmarks/*.transformed.js $CIRCLE_ARTIFACTS/benchmarks 15 | - npm run coverage:html -- --report-dir $CIRCLE_ARTIFACTS/coverage 16 | - npm run coverage:codecov && bash <(curl -s https://codecov.io/bash) 17 | checkout: 18 | post: 19 | - git submodule sync --recursive 20 | - git submodule update --recursive --init 21 | deployment: 22 | prod: 23 | branch: master 24 | commands: 25 | - git show-ref --head --heads | while IFS=' ' read -r hash name; do test ! -e "${GIT_DIR:-.git}/$name" && echo $hash > "${GIT_DIR:-.git}/$name"; done 26 | - npm run semantic-release || true 27 | -------------------------------------------------------------------------------- /decls/babel-misc.js: -------------------------------------------------------------------------------- 1 | import babelTemplate from 'babel-template'; 2 | declare type BabelTemplate = typeof babelTemplate; // eslint-disable-line no-undef 3 | 4 | import babelTypes from 'babel-types'; 5 | declare type BabelTypes = typeof babelTypes; // eslint-disable-line no-undef 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-transform-hoist-nested-functions", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Babel plugin to hoist nested functions", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/motiz88/babel-plugin-transform-hoist-nested-functions.git" 8 | }, 9 | "author": "motiz88 ", 10 | "main": "lib/index.js", 11 | "files": [ 12 | "lib" 13 | ], 14 | "scripts": { 15 | "benchmark:save": "npm run benchmark > benchmark.log", 16 | "benchmark": "cross-env NODE_ENV=production babel-node scripts/benchmark", 17 | "build": "cross-env BABEL_ENV=production babel src -d lib", 18 | "clean": "shx rm -rf lib", 19 | "coverage:codecov": "nyc report --reporter=text-lcov > coverage.lcov", 20 | "coverage:html": "nyc report --reporter=html", 21 | "download-flow-libs": "cross-env NODE_ENV=production babel-node scripts/download-flow-libs", 22 | "eslint:xunit-to-file": "envcheck ESLINT_FILE && compat \"npm run eslint -- --quiet -f junit --output-file $ESLINT_FILE\"", 23 | "eslint": "eslint *.js src test decls scripts benchmarks", 24 | "example": "npm run build && babel example/input.js -o example/output.js", 25 | "flow:check": "flow check", 26 | "flow": "flow", 27 | "generate-fixtures": "babel-node scripts/generate-fixtures", 28 | "prepublish": "npm run clean && npm run build", 29 | "semantic-release": "semantic-release pre && npm run build && npm publish && semantic-release post", 30 | "test:ci": "run-s download-flow-libs test:coverage-and-xunit flow:check eslint:xunit-to-file benchmark:save", 31 | "test:coverage-and-xunit": "cross-env NODE_ENV=test nyc --silent mocha --reporter mocha-junit-reporter", 32 | "test:coverage": "cross-env NODE_ENV=test nyc --silent mocha", 33 | "test:debug": "node-debug --no-preload --web-port 8083 _mocha", 34 | "test:fast": "mocha", 35 | "test:local": "run-s download-flow-libs test:coverage coverage:html eslint flow benchmark", 36 | "test": "npm run test:ci" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "^6.9.0", 40 | "babel-core": "^6.18.0", 41 | "babel-eslint": "^7.2.1", 42 | "babel-plugin-istanbul": "^4.1.1", 43 | "babel-plugin-syntax-class-properties": "^6.13.0", 44 | "babel-plugin-syntax-flow": "^6.13.0", 45 | "babel-plugin-syntax-jsx": "^6.13.0", 46 | "babel-plugin-transform-async-to-generator": "^6.8.0", 47 | "babel-plugin-transform-class-properties": "^6.11.5", 48 | "babel-plugin-transform-es2015-classes": "^6.14.0", 49 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 50 | "babel-preset-es2015-node4": "^2.1.0", 51 | "babel-preset-react": "^6.11.1", 52 | "babel-register": "^6.14.0", 53 | "benchmark": "^2.1.1", 54 | "compat": "^1.0.3", 55 | "condition-circle": "^1.5.0", 56 | "cross-env": "^4.0.0", 57 | "deep-assign": "^2.0.0", 58 | "env-check": "0.0.1", 59 | "enzyme": "^2.4.1", 60 | "eslint": "^3.5.0", 61 | "eslint-config-semistandard": "^8.0.0", 62 | "eslint-config-standard": "^7.1.0", 63 | "eslint-plugin-babel": "^4.1.1", 64 | "eslint-plugin-flowtype": "^2.16.1", 65 | "eslint-plugin-mocha": "^4.5.1", 66 | "eslint-plugin-promise": "^3.5.0", 67 | "eslint-plugin-standard": "^2.0.0", 68 | "flow-bin": "^0.42.0", 69 | "fs-promise": "^2.0.1", 70 | "jsdom": "^9.5.0", 71 | "microtime": "^2.1.1", 72 | "mocha": "^3.0.2", 73 | "mocha-junit-reporter": "^1.12.0", 74 | "mocha-lcov-reporter": "^1.2.0", 75 | "npm-run-all": "^4.0.2", 76 | "nyc": "^10.2.0", 77 | "react": "^15.3.1", 78 | "react-addons-test-utils": "^15.3.1", 79 | "react-dom": "^15.3.1", 80 | "request": "^2.74.0", 81 | "request-promise-native": "^1.0.3", 82 | "semantic-release": "^6.3.2", 83 | "shx": "^0.2.2" 84 | }, 85 | "keywords": [ 86 | "babel", 87 | "plugin", 88 | "babel-plugin" 89 | ], 90 | "license": "MIT", 91 | "nyc": { 92 | "all": true, 93 | "include": "src/**/*.js", 94 | "sourceMap": false, 95 | "instrument": false 96 | }, 97 | "release": { 98 | "verifyConditions": "condition-circle" 99 | }, 100 | "engines": { 101 | "node": ">=4" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /scripts/benchmark.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { transformFileSync } from 'babel-core'; 4 | import { Suite } from 'benchmark'; 5 | import fixturesBabelConfig from '../test/fixtures-babel-config'; 6 | 7 | const benchmarksDir = path.join(__dirname, '../benchmarks'); 8 | fs.readdirSync(benchmarksDir).forEach((benchmarkName) => { 9 | if (benchmarkName.endsWith('.transformed.js')) return; 10 | const benchmarkFile = path.resolve(benchmarksDir, benchmarkName); 11 | 12 | const unmodified = setup(require(benchmarkFile)); 13 | const hoistedCode = transformFileSync(benchmarkFile, fixturesBabelConfig()).code; 14 | const hoistedFile = path.join(benchmarksDir, path.basename(benchmarkName, '.js') + '.transformed.js'); 15 | fs.writeFileSync(hoistedFile, hoistedCode); 16 | const hoisted = setup(require(hoistedFile)); 17 | 18 | const suite = new Suite(benchmarkName); 19 | suite 20 | .add('unmodified', unmodified, { minSamples: 25 }) 21 | .add('hoisted', hoisted, { minSamples: 25 }) 22 | .on('start', function (event) { 23 | console.log(this.name); // eslint-disable-line no-console 24 | }) 25 | .on('cycle', function (event) { 26 | console.log('-', String(event.target)); // eslint-disable-line no-console 27 | }) 28 | .on('complete', function () { 29 | console.log(' ', this.filter('fastest').map('name') + ' is fastest'); // eslint-disable-line no-console 30 | }) 31 | .run(); 32 | }); 33 | 34 | function setup (fn) { 35 | return fn; 36 | } 37 | -------------------------------------------------------------------------------- /scripts/download-flow-libs.js: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import path from 'path'; 3 | import fs from 'fs-promise'; 4 | import pkgBabelTypes from 'babel-types/package.json'; 5 | 6 | const {version} = pkgBabelTypes; 7 | 8 | const files = [{ 9 | url: `https://raw.githubusercontent.com/babel/babel/v${version}/lib/types.js`, 10 | file: path.resolve(__dirname, `../decls/babel/v${version}/types.js`) 11 | }]; 12 | 13 | (async function () { 14 | for (const {url, file} of files) { 15 | if (!await fs.exists(file)) { 16 | process.stdout.write(`${url} --> ${path.relative(process.cwd(), file)}`); 17 | await fs.ensureDir(path.dirname(file)); 18 | await new Promise((resolve, reject) => request(url) 19 | .pipe(fs.createWriteStream(file)) 20 | .on('error', e => reject(e)) 21 | .on('finish', () => { 22 | resolve(); 23 | }) 24 | ); 25 | } 26 | process.stdout.write('\n'); 27 | } 28 | process.stdout.write('\n'); 29 | })() 30 | .catch(e => { 31 | throw e; 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/generate-fixtures.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { transformFileSync } from 'babel-core'; 4 | import fixturesBabelConfig from '../test/fixtures-babel-config'; 5 | 6 | const only = process.argv.slice(2); 7 | 8 | const fixturesDir = path.join(__dirname, '../test/fixtures'); 9 | fs.readdirSync(fixturesDir).forEach((caseName) => { 10 | if (only.length && only.indexOf(caseName) === -1) return; 11 | process.stdout.write(caseName + '\n'); 12 | const fixtureDir = path.join(fixturesDir, caseName); 13 | const optionsPath = path.join(fixtureDir, 'options.json'); 14 | const options = fixturesBabelConfig(fs.existsSync(optionsPath) ? JSON.parse(fs.readFileSync(optionsPath).toString()) : {}); 15 | const actualPath = path.join(fixtureDir, 'actual.js'); 16 | const actual = transformFileSync(actualPath, options).code; 17 | 18 | fs.writeFileSync( 19 | path.join(fixtureDir, 'expected.js'), 20 | actual 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { NodePath, Scope } from 'babel-traverse'; 4 | 5 | class InnerScopeVisitor { 6 | referencedScopes: Scope[]; 7 | thisReferencedScopes: Scope[]; 8 | 9 | ReferencedIdentifier = (path: NodePath) => { 10 | const binding = path.scope.getBinding(path.node.name); 11 | if (binding) { 12 | // istanbul ignore next: could be initialized elsewhere 13 | if (!this.referencedScopes) { 14 | this.referencedScopes = []; 15 | } 16 | this.referencedScopes.push(binding.scope); 17 | } 18 | } 19 | 20 | ThisExpression = (path: NodePath) => { 21 | let {scope} = path; 22 | while (scope && (scope = scope.getFunctionParent())) { // eslint-disable-line no-cond-assign 23 | if (!scope.path.isArrowFunctionExpression()) { 24 | if (!this.thisReferencedScopes) { 25 | this.thisReferencedScopes = []; 26 | } 27 | this.thisReferencedScopes.push(scope); 28 | return; 29 | } 30 | scope = scope.parent; 31 | } 32 | } 33 | } 34 | 35 | function uniqueScopes (scopes: Scope[]): Scope[] { 36 | return scopes 37 | .sort((a, b) => (a.uid - b.uid)) 38 | .reduce((a, x) => (a.length && a[a.length - 1].uid === x.uid) ? a : a.concat(x), []); 39 | } 40 | 41 | function deepestScopeOf (path: NodePath, scopes: Scope[]): ?Scope { 42 | let scope = path.scope; 43 | do { 44 | if (scopes.indexOf(scope) !== -1) { 45 | return scope; 46 | } 47 | } while (scope = scope.parent); // eslint-disable-line no-cond-assign 48 | } 49 | 50 | type BabelVisitors = { 51 | [key: string]: (path: NodePath) => void 52 | }; 53 | 54 | type BabelPlugin = { 55 | visitor?: BabelVisitors 56 | }; 57 | 58 | export default function ({types: t, template}: {types: BabelTypes, template: BabelTemplate}): BabelPlugin { 59 | const declarationTemplate = template(` 60 | var NAME = VALUE; 61 | `); 62 | const symbolTemplate = template(` 63 | new Symbol(NAME) 64 | `); 65 | const thisMemberReferenceTemplate = template(` 66 | this[METHOD] 67 | `); 68 | return { 69 | visitor: { 70 | Function (path: NodePath) { 71 | if (t.isProgram(path.scope.parent.block)) { 72 | // Nowhere we can move this function 73 | return; 74 | } 75 | if (path.getData('unhoistable')) return; 76 | // Determine the outermost scope we can move to, 77 | // which is the innermost scope we are actually referencing (if any), 78 | // or the global scope. 79 | const innerScope = new InnerScopeVisitor(); 80 | path.traverse(innerScope); 81 | const thisReferencedScopes = uniqueScopes(innerScope.thisReferencedScopes || []); 82 | const referencedScopes = uniqueScopes(innerScope.referencedScopes || []); 83 | const allReferencedScopes = uniqueScopes([ 84 | ...(innerScope.referencedScopes || []), 85 | ...thisReferencedScopes 86 | ]); 87 | const targetScope = deepestScopeOf( 88 | path, 89 | allReferencedScopes 90 | .concat(path.scope.getProgramParent()) 91 | .filter(scope => scope !== path.scope) 92 | ); 93 | if (!targetScope) return; 94 | if (targetScope === path.scope.parent) { 95 | if ( 96 | this.opts.methods && 97 | targetScope.path.isClassMethod() && 98 | thisReferencedScopes.indexOf(targetScope) !== -1 && 99 | referencedScopes.indexOf(targetScope) === -1 100 | ) { 101 | const parentScope: Scope = targetScope.parent; 102 | const containingClassBodyPath: NodePath = targetScope.path.parentPath; 103 | const id = parentScope.generateUidIdentifierBasedOnNode(path.node.id || path.node, 'hoistedMethod'); 104 | parentScope.push({kind: 'const', id, init: symbolTemplate({NAME: t.stringLiteral(id.name)}).expression}); 105 | const prop = t.classProperty(id, Object.assign({}, path.node, {shadow: true}), null, null, true); 106 | containingClassBodyPath.unshiftContainer('body', prop); 107 | path.replaceWith(thisMemberReferenceTemplate({METHOD: id}).expression); 108 | } 109 | return; 110 | } 111 | if (path.node.id) { 112 | const binding = path.scope.getBinding(path.node.id.name); 113 | if (!binding) return; 114 | const isUnsafeUse = usePath => { 115 | if (usePath.parent.type !== 'MemberExpression') return false; 116 | if (usePath.key !== 'object') return false; 117 | while (usePath = usePath.parentPath) { // eslint-disable-line no-cond-assign 118 | if (usePath.parent.type === 'UpdateExpression' && usePath.key === 'argument') { 119 | return true; 120 | } 121 | if (usePath.parent.type === 'AssignmentExpression' && usePath.key === 'left') { 122 | return true; 123 | } 124 | if (!usePath.isLVal()) { 125 | break; 126 | } 127 | } 128 | return false; 129 | }; 130 | const uses = binding.referencePaths 131 | .filter(refPath => refPath !== path && refPath.isIdentifier()); 132 | 133 | if (uses.length) { 134 | if (uses.some(isUnsafeUse)) { 135 | return; 136 | } 137 | } 138 | } 139 | 140 | // FunctionDeclaration needs some extra processing to be compatible with Babel's hoister: 141 | // 1) Replace the node with a VariableDeclaration initialized with a FunctionExpression. 142 | // 2) Invoke hoist() on the expression. 143 | // 3) Hoist the declaration itself within its block so the function is correctly available 144 | // even before its original point of declaration. 145 | 146 | let declaratorPath: ?NodePath = null; 147 | let declaratorBackupNode: ?BabelNode = null; 148 | if (path.isFunctionDeclaration()) { 149 | declaratorPath = path; 150 | declaratorBackupNode = declaratorPath.node; 151 | path.replaceWith( 152 | declarationTemplate( 153 | { 154 | NAME: path.node.id, 155 | VALUE: Object.assign({}, path.node, {type: 'FunctionExpression'}) 156 | } 157 | ) 158 | ); 159 | path = declaratorPath.get('declarations.0.init'); 160 | } 161 | 162 | // Babel's hoister will give the outer declaration a meaningless id like "_ref" :( 163 | // We generate a meaningful one instead, and we do it *before* hoisting so as to completely 164 | // ignore the hoister's id (in case ours happen to collide with it). 165 | const id = targetScope.generateUidIdentifierBasedOnNode(path.node.id || path.node, 'hoistedAnonymousFunc'); 166 | path.hoist(targetScope); 167 | // fix up the temporary hoisted name to our meaningful id 168 | const tempName: string = path.node.name; 169 | if (!tempName) { 170 | // hoisting did not succeed 171 | if (declaratorBackupNode && declaratorPath) { 172 | declaratorPath.setData('unhoistable', true); 173 | declaratorPath.replaceWith(declaratorBackupNode); 174 | } 175 | return; 176 | } 177 | targetScope.crawl(); // so the next line can find the newly inserted binding 178 | const tempBinding = path.scope.getBinding(tempName); 179 | tempBinding.path.get('id').replaceWith(id); 180 | path.replaceWith(id); 181 | path.scope.crawl(); // so we see the renamed reference in further processing 182 | tempBinding.path.scope.crawl(); // so the scope holding the temp binding is fully updated too 183 | 184 | // fix up the inner VariableDeclarator's position 185 | if (declaratorPath) { 186 | const { node } = declaratorPath; 187 | declaratorPath.remove(); 188 | declaratorPath.scope.getFunctionParent().path.get('body').unshiftContainer('body', node); 189 | } 190 | } 191 | } 192 | }; 193 | } 194 | -------------------------------------------------------------------------------- /test/fixtures-babel-config.js: -------------------------------------------------------------------------------- 1 | import plugin from '../src'; 2 | import merge from 'deep-assign'; 3 | 4 | const PLUGIN_NAME = require('../package.json').name; 5 | 6 | function pluginNameNormalize (s) { 7 | if (typeof s !== 'string') return s; 8 | return s.replace(/^babel-plugin-/, ''); 9 | } 10 | 11 | function pluginNameMatch (s1, s2) { 12 | const result = pluginNameNormalize(s1) === pluginNameNormalize(s2); 13 | return result; 14 | } 15 | 16 | export default (options) => { 17 | const merged = merge({}, {babelrc: false}, options || {}); 18 | if (merged.plugins) { 19 | const i = merged.plugins 20 | .findIndex(optPlugin => 21 | pluginNameMatch(optPlugin, PLUGIN_NAME) || 22 | (optPlugin === plugin) || 23 | (Array.isArray(optPlugin) && ( 24 | pluginNameMatch(optPlugin[0], PLUGIN_NAME) || 25 | (optPlugin[0] === plugin))) 26 | ); 27 | if (i === -1) { 28 | merged.plugins.unshift(plugin); 29 | } else { 30 | if (Array.isArray(merged.plugins[i])) { 31 | merged.plugins[i][0] = plugin; 32 | } else { 33 | merged.plugins[i] = plugin; 34 | } 35 | } 36 | } else { 37 | merged.plugins = [plugin]; 38 | } 39 | return merged; 40 | }; 41 | -------------------------------------------------------------------------------- /test/fixtures/handle-naming-edge-cases/actual.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // NOTE: hoisted 3 | return function ref(param) { 4 | x = 2 * param; 5 | } 6 | })(); 7 | -------------------------------------------------------------------------------- /test/fixtures/handle-naming-edge-cases/expected.js: -------------------------------------------------------------------------------- 1 | var _ref = function ref(param) { 2 | x = 2 * param; 3 | }; 4 | 5 | (function () { 6 | // NOTE: hoisted 7 | return _ref; 8 | })(); -------------------------------------------------------------------------------- /test/fixtures/handle-references-to-this/actual.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // NOTE: not hoisted 3 | return () => this; 4 | })(); 5 | 6 | class A { 7 | method() { 8 | // NOTE: not hoisted 9 | return () => this; 10 | } 11 | } 12 | 13 | (function A () { 14 | // NOTE: hoisted 15 | return () => 16 | // NOTE: hoisted 17 | function B () { 18 | return this; 19 | }; 20 | })(); 21 | 22 | // NOTE: not hoisted 23 | () => this; 24 | -------------------------------------------------------------------------------- /test/fixtures/handle-references-to-this/expected.js: -------------------------------------------------------------------------------- 1 | var _this = this; 2 | 3 | (function () { 4 | // NOTE: not hoisted 5 | return () => this; 6 | })(); 7 | 8 | class A { 9 | method() { 10 | // NOTE: not hoisted 11 | return () => this; 12 | } 13 | } 14 | 15 | var 16 | // NOTE: hoisted 17 | _B = function B() { 18 | return this; 19 | }; 20 | 21 | var _hoistedAnonymousFunc2 = () => _B; 22 | 23 | (function A() { 24 | // NOTE: hoisted 25 | return _hoistedAnonymousFunc2; 26 | })(); 27 | 28 | // NOTE: not hoisted 29 | () => _this; -------------------------------------------------------------------------------- /test/fixtures/hoist-async-functions/actual.js: -------------------------------------------------------------------------------- 1 | (async function () { 2 | // NOTE: hoisted 3 | async function inner() {} 4 | return inner; 5 | })(); 6 | 7 | (async function () { 8 | return inner; 9 | // NOTE: hoisted 10 | async function inner() {} 11 | })(); 12 | 13 | (async function () { 14 | // NOTE: hoisted 15 | return async function inner() {}; 16 | })(); 17 | 18 | (async function () { 19 | // NOTE: hoisted 20 | return async function () {}; 21 | })(); 22 | 23 | (async function () { 24 | // NOTE: hoisted 25 | return async () => {}; 26 | })(); 27 | 28 | (async function () { 29 | // NOTE: hoisted 30 | const x = async () => {}; 31 | return x; 32 | })(); 33 | -------------------------------------------------------------------------------- /test/fixtures/hoist-async-functions/expected.js: -------------------------------------------------------------------------------- 1 | var 2 | // NOTE: hoisted 3 | _inner = async function inner() {}; 4 | 5 | (async function () { 6 | var inner = _inner; 7 | 8 | return inner; 9 | })(); 10 | 11 | var 12 | // NOTE: hoisted 13 | _inner2 = async function inner() {}; 14 | 15 | (async function () { 16 | var inner = _inner2; 17 | 18 | return inner; 19 | })(); 20 | 21 | var _inner3 = async function inner() {}; 22 | 23 | (async function () { 24 | // NOTE: hoisted 25 | return _inner3; 26 | })(); 27 | 28 | var _hoistedAnonymousFunc = async function () {}; 29 | 30 | (async function () { 31 | // NOTE: hoisted 32 | return _hoistedAnonymousFunc; 33 | })(); 34 | 35 | var _hoistedAnonymousFunc2 = async () => {}; 36 | 37 | (async function () { 38 | // NOTE: hoisted 39 | return _hoistedAnonymousFunc2; 40 | })(); 41 | 42 | var _hoistedAnonymousFunc3 = async () => {}; 43 | 44 | (async function () { 45 | // NOTE: hoisted 46 | const x = _hoistedAnonymousFunc3; 47 | return x; 48 | })(); -------------------------------------------------------------------------------- /test/fixtures/hoist-functions/actual.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // NOTE: hoisted 3 | function inner() {} 4 | return inner; 5 | })(); 6 | 7 | (function () { 8 | return inner; 9 | // NOTE: hoisted 10 | function inner() {} 11 | })(); 12 | 13 | (function () { 14 | // NOTE: hoisted 15 | return function inner() {}; 16 | })(); 17 | 18 | (function () { 19 | // NOTE: hoisted 20 | return function () {}; 21 | })(); 22 | 23 | (function () { 24 | // NOTE: hoisted 25 | return () => {}; 26 | })(); 27 | 28 | (function () { 29 | // NOTE: hoisted 30 | const x = () => {}; 31 | return x; 32 | })(); 33 | -------------------------------------------------------------------------------- /test/fixtures/hoist-functions/expected.js: -------------------------------------------------------------------------------- 1 | var 2 | // NOTE: hoisted 3 | _inner = function inner() {}; 4 | 5 | (function () { 6 | var inner = _inner; 7 | 8 | return inner; 9 | })(); 10 | 11 | var 12 | // NOTE: hoisted 13 | _inner2 = function inner() {}; 14 | 15 | (function () { 16 | var inner = _inner2; 17 | 18 | return inner; 19 | })(); 20 | 21 | var _inner3 = function inner() {}; 22 | 23 | (function () { 24 | // NOTE: hoisted 25 | return _inner3; 26 | })(); 27 | 28 | var _hoistedAnonymousFunc = function () {}; 29 | 30 | (function () { 31 | // NOTE: hoisted 32 | return _hoistedAnonymousFunc; 33 | })(); 34 | 35 | var _hoistedAnonymousFunc2 = () => {}; 36 | 37 | (function () { 38 | // NOTE: hoisted 39 | return _hoistedAnonymousFunc2; 40 | })(); 41 | 42 | var _hoistedAnonymousFunc3 = () => {}; 43 | 44 | (function () { 45 | // NOTE: hoisted 46 | const x = _hoistedAnonymousFunc3; 47 | return x; 48 | })(); -------------------------------------------------------------------------------- /test/fixtures/hoist-generator-functions/actual.js: -------------------------------------------------------------------------------- 1 | (function * () { 2 | // NOTE: hoisted 3 | function * inner() {} 4 | return inner; 5 | })(); 6 | 7 | (function * () { 8 | return inner; 9 | // NOTE: hoisted 10 | function * inner() {} 11 | })(); 12 | 13 | (function * () { 14 | // NOTE: hoisted 15 | return function * inner() {}; 16 | })(); 17 | 18 | (function * () { 19 | // NOTE: hoisted 20 | return function * () {}; 21 | })(); 22 | -------------------------------------------------------------------------------- /test/fixtures/hoist-generator-functions/expected.js: -------------------------------------------------------------------------------- 1 | var 2 | // NOTE: hoisted 3 | _inner = function* inner() {}; 4 | 5 | (function* () { 6 | var inner = _inner; 7 | 8 | return inner; 9 | })(); 10 | 11 | var 12 | // NOTE: hoisted 13 | _inner2 = function* inner() {}; 14 | 15 | (function* () { 16 | var inner = _inner2; 17 | 18 | return inner; 19 | })(); 20 | 21 | var _inner3 = function* inner() {}; 22 | 23 | (function* () { 24 | // NOTE: hoisted 25 | return _inner3; 26 | })(); 27 | 28 | var _hoistedAnonymousFunc = function* () {}; 29 | 30 | (function* () { 31 | // NOTE: hoisted 32 | return _hoistedAnonymousFunc; 33 | })(); -------------------------------------------------------------------------------- /test/fixtures/hoist-nested-methods-if-options.methods-true/actual.js: -------------------------------------------------------------------------------- 1 | class A { 2 | outer () { 3 | // NOTE: hoisted 4 | (function () {})(); 5 | } 6 | } 7 | 8 | class B { 9 | outer () { 10 | // NOTE: hoisted to bound method 11 | (() => this)(); 12 | } 13 | } 14 | 15 | class C { 16 | static outer () { 17 | // NOTE: hoisted to static method 18 | console.log((() => this)()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/hoist-nested-methods-if-options.methods-true/expected.js: -------------------------------------------------------------------------------- 1 | const _hoistedMethod = new Symbol("_hoistedMethod"), 2 | _hoistedMethod2 = new Symbol("_hoistedMethod2"); 3 | 4 | var _hoistedAnonymousFunc2 = function () {}; 5 | 6 | class A { 7 | outer() { 8 | // NOTE: hoisted 9 | _hoistedAnonymousFunc2(); 10 | } 11 | } 12 | 13 | class B { 14 | [_hoistedMethod] = () => this; 15 | 16 | outer() { 17 | // NOTE: hoisted to bound method 18 | this[_hoistedMethod](); 19 | } 20 | } 21 | 22 | class C { 23 | [_hoistedMethod2] = () => this; 24 | 25 | static outer() { 26 | // NOTE: hoisted to static method 27 | console.log(this[_hoistedMethod2]()); 28 | } 29 | } -------------------------------------------------------------------------------- /test/fixtures/hoist-nested-methods-if-options.methods-true/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [["transform-hoist-nested-functions", {"methods": true}]] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/hoist-out-of-loops/actual.js: -------------------------------------------------------------------------------- 1 | do { 2 | // FIXME: This is not hoisted but probably should be 3 | function inner(param) {} 4 | } while (false); 5 | 6 | (function () { 7 | do { 8 | // NOTE: This is hoisted 9 | function inner(param) {} 10 | } while (false); 11 | })(); 12 | 13 | do { 14 | // FIXME: This is not hoisted but probably should be 15 | function inner(param) { 16 | // NOTE: This is hoisted 17 | function deepInner(deepParam) {} 18 | } 19 | } while (false); 20 | 21 | for (;;) { 22 | // FIXME: This is not hoisted but probably should be 23 | function inner(param) {} 24 | } 25 | 26 | (function () { 27 | for (;;) { 28 | // NOTE: This is hoisted 29 | function inner(param) {} 30 | } 31 | })(); 32 | 33 | for (;;) { 34 | // FIXME: This is not hoisted but probably should be 35 | function inner(param) { 36 | // NOTE: This is hoisted 37 | function deepInner(deepParam) {} 38 | } 39 | } -------------------------------------------------------------------------------- /test/fixtures/hoist-out-of-loops/expected.js: -------------------------------------------------------------------------------- 1 | do { 2 | // FIXME: This is not hoisted but probably should be 3 | function inner(param) {} 4 | } while (false); 5 | 6 | var 7 | // NOTE: This is hoisted 8 | _inner2 = function inner(param) {}; 9 | 10 | (function () { 11 | var inner = _inner2; 12 | 13 | do {} while (false); 14 | })(); 15 | 16 | var 17 | // NOTE: This is hoisted 18 | _deepInner = function deepInner(deepParam) {}; 19 | 20 | do { 21 | // FIXME: This is not hoisted but probably should be 22 | function inner(param) { 23 | var deepInner = _deepInner; 24 | } 25 | } while (false); 26 | 27 | for (;;) { 28 | // FIXME: This is not hoisted but probably should be 29 | function inner(param) {} 30 | } 31 | 32 | var 33 | // NOTE: This is hoisted 34 | _inner3 = function inner(param) {}; 35 | 36 | (function () { 37 | var inner = _inner3; 38 | 39 | for (;;) {} 40 | })(); 41 | 42 | var 43 | // NOTE: This is hoisted 44 | _deepInner2 = function deepInner(deepParam) {}; 45 | 46 | for (;;) { 47 | // FIXME: This is not hoisted but probably should be 48 | function inner(param) { 49 | var deepInner = _deepInner2; 50 | } 51 | } -------------------------------------------------------------------------------- /test/fixtures/not-hoist-mutated-funcs/actual.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // NOTE: not hoisted 3 | function inner(param) {} 4 | inner.someProp++; 5 | })(); 6 | 7 | (function () { 8 | // NOTE: hoisted 9 | function inner(param) {} 10 | return inner.name; 11 | })(); 12 | 13 | (function () { 14 | function inner(param) {} 15 | // NOTE: not hoisted 16 | inner.someProp = 1; 17 | })(); 18 | 19 | (function () { 20 | // NOTE: hoisted 21 | function inner(param) {} 22 | 23 | var dummy = {}; 24 | return dummy[inner]; 25 | })(); 26 | 27 | (function () { 28 | // NOTE: hoisted 29 | function inner(param) {} 30 | inner; 31 | })(); 32 | 33 | (function () { 34 | // NOTE: hoisted 35 | function inner(param) {} 36 | inner.name; 37 | })(); 38 | 39 | (class { 40 | outer() { 41 | // FIXME: unsafely hoisted 42 | const inner = () => {}; 43 | inner.someProp = 1; 44 | } 45 | }); 46 | 47 | (class { 48 | outer() { 49 | // NOTE: hoisted to bound method 50 | const inner = () => this.constructor.name; 51 | inner.name; 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /test/fixtures/not-hoist-mutated-funcs/expected.js: -------------------------------------------------------------------------------- 1 | const _hoistedMethod = new Symbol("_hoistedMethod"); 2 | 3 | (function () { 4 | // NOTE: not hoisted 5 | function inner(param) {} 6 | inner.someProp++; 7 | })(); 8 | 9 | var 10 | // NOTE: hoisted 11 | _inner = function inner(param) {}; 12 | 13 | (function () { 14 | var inner = _inner; 15 | 16 | return inner.name; 17 | })(); 18 | 19 | (function () { 20 | function inner(param) {} 21 | // NOTE: not hoisted 22 | inner.someProp = 1; 23 | })(); 24 | 25 | var 26 | // NOTE: hoisted 27 | _inner2 = function inner(param) {}; 28 | 29 | (function () { 30 | var inner = _inner2; 31 | 32 | 33 | var dummy = {}; 34 | return dummy[inner]; 35 | })(); 36 | 37 | var 38 | // NOTE: hoisted 39 | _inner3 = function inner(param) {}; 40 | 41 | (function () { 42 | var inner = _inner3; 43 | 44 | inner; 45 | })(); 46 | 47 | var 48 | // NOTE: hoisted 49 | _inner4 = function inner(param) {}; 50 | 51 | (function () { 52 | var inner = _inner4; 53 | 54 | inner.name; 55 | })(); 56 | 57 | var _hoistedAnonymousFunc2 = () => {}; 58 | 59 | (class { 60 | outer() { 61 | // FIXME: unsafely hoisted 62 | const inner = _hoistedAnonymousFunc2; 63 | inner.someProp = 1; 64 | } 65 | }); 66 | 67 | (class { 68 | [_hoistedMethod] = () => this.constructor.name; 69 | 70 | outer() { 71 | // NOTE: hoisted to bound method 72 | const inner = this[_hoistedMethod]; 73 | inner.name; 74 | } 75 | }); -------------------------------------------------------------------------------- /test/fixtures/not-hoist-mutated-funcs/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [["transform-hoist-nested-functions", {"methods": true}]] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/not-hoist-nested-methods-by-default/actual.js: -------------------------------------------------------------------------------- 1 | class A { 2 | outer () { 3 | // NOTE: hoisted 4 | (function () {})(); 5 | } 6 | } 7 | 8 | class B { 9 | outer () { 10 | // NOTE: not hoisted (!options.methods) 11 | (() => this)(); 12 | } 13 | } 14 | 15 | class C { 16 | static outer () { 17 | // NOTE: not hoisted (!options.methods) 18 | console.log((() => this)()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/not-hoist-nested-methods-by-default/expected.js: -------------------------------------------------------------------------------- 1 | var _hoistedAnonymousFunc2 = function () {}; 2 | 3 | class A { 4 | outer() { 5 | // NOTE: hoisted 6 | _hoistedAnonymousFunc2(); 7 | } 8 | } 9 | 10 | class B { 11 | outer() { 12 | // NOTE: not hoisted (!options.methods) 13 | (() => this)(); 14 | } 15 | } 16 | 17 | class C { 18 | static outer() { 19 | // NOTE: not hoisted (!options.methods) 20 | console.log((() => this)()); 21 | } 22 | } -------------------------------------------------------------------------------- /test/fixtures/not-hoist-nested-methods-by-default/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-hoist-nested-functions"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/not-hoist-nested-methods-if-options.methods-false/actual.js: -------------------------------------------------------------------------------- 1 | class A { 2 | outer () { 3 | // NOTE: hoisted 4 | (function () {})(); 5 | } 6 | } 7 | 8 | class B { 9 | outer () { 10 | // NOTE: not hoisted (!options.methods) 11 | (() => this)(); 12 | } 13 | } 14 | 15 | class C { 16 | static outer () { 17 | // NOTE: not hoisted 18 | console.log((() => this)()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/not-hoist-nested-methods-if-options.methods-false/expected.js: -------------------------------------------------------------------------------- 1 | var _hoistedAnonymousFunc2 = function () {}; 2 | 3 | class A { 4 | outer() { 5 | // NOTE: hoisted 6 | _hoistedAnonymousFunc2(); 7 | } 8 | } 9 | 10 | class B { 11 | outer() { 12 | // NOTE: not hoisted (!options.methods) 13 | (() => this)(); 14 | } 15 | } 16 | 17 | class C { 18 | static outer() { 19 | // NOTE: not hoisted 20 | console.log((() => this)()); 21 | } 22 | } -------------------------------------------------------------------------------- /test/fixtures/not-hoist-nested-methods-if-options.methods-false/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [["transform-hoist-nested-functions", {"methods": false}]] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/react-render-callback/actual.js: -------------------------------------------------------------------------------- 1 | class A { 2 | render() { 3 | return 4 | { 5 | // NOTE: hoisted 6 | (val, set) => 7 |
set(val + 1) 10 | }> 11 | clicked {val} times 12 |
13 | } 14 |
; 15 | } 16 | } 17 | 18 | class B { 19 | render() { 20 | // NOTE: The use of a captured variable blocks hoisting out of render() 21 | const localClicked = 'clicked'; 22 | return 23 | { 24 | // NOTE: not hoisted 25 | (val, set) => 26 |
set(val + 1) 29 | }> 30 | {localClicked} {val} times 31 |
32 | } 33 |
; 34 | } 35 | } 36 | 37 | class C { 38 | render() { 39 | return 40 | { 41 | // NOTE: hoisted to bound bethod 42 | (val) => 43 |
this.set(val + 1) 46 | }> 47 | clicked {val} times 48 |
49 | } 50 |
; 51 | } 52 | 53 | set() {} 54 | } 55 | -------------------------------------------------------------------------------- /test/fixtures/react-render-callback/expected.js: -------------------------------------------------------------------------------- 1 | const _hoistedMethod = new Symbol('_hoistedMethod'); 2 | 3 | var 4 | // NOTE: hoisted 5 | _hoistedAnonymousFunc2 = (val, set) =>
set(val + 1)}> 8 | clicked {val} times 9 |
; 10 | 11 | class A { 12 | render() { 13 | return 14 | {_hoistedAnonymousFunc2} 15 | ; 16 | } 17 | } 18 | 19 | class B { 20 | render() { 21 | // NOTE: The use of a captured variable blocks hoisting out of render() 22 | const localClicked = 'clicked'; 23 | return 24 | { 25 | // NOTE: not hoisted 26 | (val, set) =>
set(val + 1)}> 29 | {localClicked} {val} times 30 |
} 31 |
; 32 | } 33 | } 34 | 35 | class C { 36 | [_hoistedMethod] = 37 | // NOTE: hoisted to bound bethod 38 | val =>
this.set(val + 1)}> 41 | clicked {val} times 42 |
; 43 | 44 | render() { 45 | return 46 | {this[_hoistedMethod]} 47 | ; 48 | } 49 | 50 | set() {} 51 | } -------------------------------------------------------------------------------- /test/fixtures/react-render-callback/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["syntax-jsx", ["transform-hoist-nested-functions", {"methods": true}]] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/regression-classes-unbound-name/actual.js: -------------------------------------------------------------------------------- 1 | class Clazz { 2 | unbound () {} 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/regression-classes-unbound-name/expected.js: -------------------------------------------------------------------------------- 1 | var _createClass = function () { var defineProperties = _defineProperties; return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | var _defineProperties = function defineProperties(target, props) { 6 | for (var i = 0; i < props.length; i++) { 7 | var descriptor = props[i]; 8 | descriptor.enumerable = descriptor.enumerable || false; 9 | descriptor.configurable = true; 10 | if ("value" in descriptor) descriptor.writable = true; 11 | Object.defineProperty(target, descriptor.key, descriptor); 12 | } 13 | }; 14 | 15 | let Clazz = function () { 16 | function Clazz() { 17 | _classCallCheck(this, Clazz); 18 | } 19 | 20 | _createClass(Clazz, [{ 21 | key: "unbound", 22 | value: function unbound() {} 23 | }]); 24 | 25 | return Clazz; 26 | }(); -------------------------------------------------------------------------------- /test/fixtures/regression-classes-unbound-name/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-es2015-classes"] 3 | } 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | test/specs/**/*.js 3 | -------------------------------------------------------------------------------- /test/specs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "plugins": [ 6 | "mocha" 7 | ], 8 | "rules": { 9 | "mocha/no-exclusive-tests": "error", 10 | "mocha/no-skipped-tests": "error" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/specs/transform-fixtures.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import assert from 'assert'; 4 | import { transformFileSync } from 'babel-core'; 5 | import fixturesBabelConfig from '../fixtures-babel-config'; 6 | 7 | function normalize (str) { 8 | return str.replace(/\r\n?/g, '\n').trim(); 9 | } 10 | 11 | describe('hoist-nested-functions', () => { 12 | const fixturesDir = path.join(__dirname, '../fixtures'); 13 | fs.readdirSync(fixturesDir).forEach((caseName) => { 14 | it(`should ${caseName.split('-').join(' ')}`, () => { 15 | const fixtureDir = path.join(fixturesDir, caseName); 16 | const actualPath = path.join(fixtureDir, 'actual.js'); 17 | const optionsPath = path.join(fixtureDir, 'options.json'); 18 | const options = fs.existsSync(optionsPath) ? JSON.parse(fs.readFileSync(optionsPath).toString()) : {}; 19 | const actual = transformFileSync(actualPath, fixturesBabelConfig(options)).code; 20 | 21 | const expected = fs.readFileSync( 22 | path.join(fixtureDir, 'expected.js') 23 | ).toString(); 24 | 25 | assert.equal(normalize(actual), normalize(expected)); 26 | }); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------