├── .gitignore ├── .prettierignore ├── .travis.yml ├── README.md ├── __testfixtures__ ├── .eslintrc.yml ├── async-await.input.js ├── async-await.output.js ├── await-promise-chain.input.js └── await-promise-chain.output.js ├── __tests__ ├── async-await-test.js └── await-promise-chain-test.js ├── async-await.js ├── await-promise-chain.js ├── lib ├── test-utils.js └── utils.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .vscode/* 4 | .npmrc 5 | 6 | # IntelliJ IDEA 7 | .idea/ 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __testfixtures__ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## async-await-codemod 2 | 3 | [![Build Status](https://img.shields.io/travis/sgilroy/async-await-codemod.svg?style=flat-square)](https://travis-ci.org/sgilroy/async-await-codemod) [![Code Style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 4 | 5 | This repository contains a codemod script for use with 6 | [JSCodeshift](https://github.com/facebook/jscodeshift) that migrates promise-based functions to use async/await syntax. 7 | 8 | The excellent [sinon-codemod](https://github.com/hurrymaplelad/sinon-codemod) repository was used as inspiration and served as a template for this repository. 9 | 10 | This codemod is based in part on work done by @cpojer https://github.com/cpojer/js-codemod/pull/49/commits/19ed546d8a47127d3d115f933d924106c98e1b8b 11 | and the further work of @cassilup https://github.com/cassilup/async-await-codemod-demo 12 | 13 | ### Setup & Run 14 | 15 | - `npm install -g jscodeshift` 16 | - `git clone https://github.com/sgilroy/async-await-codemod.git` or download a zip file 17 | from `https://github.com/sgilroy/async-await-codemod/archive/master.zip` 18 | - Run `npm install` in the async-await-codemod directory 19 | - Alternatively, run [`yarn`](https://yarnpkg.com/) to install in the 20 | async-await-codemod directory 21 | - `jscodeshift -t ` 22 | - Use the `-d` option for a dry-run and use `-p` to print the output 23 | for comparison 24 | 25 | ### async-await 26 | 27 | ES2017 natively supports a special syntax for working with promises called "async/await". 28 | 29 | With promises: 30 | 31 | ```js 32 | function makeRequest() { 33 | return getJSON().then(data => { 34 | console.log(data); 35 | return 'done'; 36 | }); 37 | } 38 | ``` 39 | 40 | With async/await: 41 | 42 | ```js 43 | async function makeRequestFunction() { 44 | const data = await getJSON(); 45 | console.log(data); 46 | return 'done'; 47 | } 48 | ``` 49 | 50 | ### Included Scripts 51 | 52 | #### `async-await` 53 | 54 | Converts each asynchronous function (a function which contains a `.then()` call) to `async`, and uses `await` instead 55 | of `.then()` to simplify the behavior of using promises synchronously. 56 | 57 | ```sh 58 | jscodeshift -t async-await-codemod/async-await.js 59 | ``` 60 | 61 | #### `await-promise-chain` 62 | 63 | Unravels chained promise calls of the style `foo.then().then()` as multiple `await` calls. Note that this changes the 64 | structure and scope of blocks of code and can thus result in different behavior, such as by variables being in scope 65 | that otherwise would not. 66 | 67 | This should generally be used after the `async-await` codemod, and the changes should be examined and tested carefully 68 | to avoid unwanted bugs or subtle problems. 69 | 70 | ```sh 71 | jscodeshift -t async-await-codemod/await-promise-chain.js 72 | ``` 73 | -------------------------------------------------------------------------------- /__testfixtures__/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | # Skip autoformatting to allow testing a range of import formats 3 | "prettier/prettier": "off" 4 | "no-unused-vars": "off" 5 | "no-console": "off" 6 | "no-undef": "off" 7 | -------------------------------------------------------------------------------- /__testfixtures__/async-await.input.js: -------------------------------------------------------------------------------- 1 | function a() { 2 | return b().then(c => { 3 | return c.d; 4 | }); 5 | } 6 | 7 | function thenFulfilledRejected() { 8 | return b().then(c => { 9 | return c.d; 10 | }, error => { 11 | console.error(error); 12 | }); 13 | } 14 | 15 | function thenDotCatch() { 16 | return b().then(c => { 17 | return c.d; 18 | }).catch(error => { 19 | console.error(error); 20 | }); 21 | } 22 | 23 | function embedded() { 24 | return first().then(() => { 25 | return second().then(() => { 26 | return third(); 27 | }); 28 | }); 29 | } 30 | 31 | function promiseArrowShorthand() { 32 | return asyncFunc().then(result => ({ result })); 33 | } 34 | 35 | const functionExpression = function() { 36 | return asyncFunc().then(result => result * 2); 37 | }; 38 | 39 | const functionExpressionCatch = function() { 40 | return asyncFunc().then(result => result * 2, error => console.error(error)); 41 | }; 42 | 43 | function countUserVotes(userIds) { 44 | return getUsers(userIds).then(users => { 45 | return Promise.reduce(users, (acc, user) => { 46 | return user.getVoteCount().then(count => acc + count); 47 | }); 48 | }); 49 | } 50 | 51 | function destructure(key) { 52 | return asyncFunc().then(({ [key]: result }) => { 53 | return result * 3; 54 | }); 55 | } 56 | 57 | const makeRequestFunction = function() { 58 | return getJSON() 59 | .then(data => { 60 | console.log(data); 61 | return "done"; 62 | }); 63 | }; 64 | 65 | const makeRequestArrowLong = () => { 66 | return getJSON() 67 | .then(data => { 68 | console.log(data); 69 | return "done"; 70 | }); 71 | }; 72 | 73 | const makeRequest = () => 74 | getJSON() 75 | .then(data => { 76 | console.log(data); 77 | return "done"; 78 | }); 79 | 80 | function chainEventualThen() { 81 | return Model.find().exec().then(items => { 82 | return items.map(item => item.thing); 83 | }); 84 | } 85 | 86 | app.get("/with-return", function(req, res) { 87 | return requestPromise(generateBeefFreeRecipeURL()).then(function(recipeResponse) { 88 | const recipesList = JSON.parse(recipeResponse).results; 89 | const recipe = recipesList[0]; 90 | const responseText = `
${
 91 |       cowsay.say({
 92 |         text: recipe.title
 93 |       })
 94 |     }
`; 95 | 96 | res.send(responseText); 97 | }, function(error) { 98 | console.log(error); 99 | }); 100 | }); 101 | 102 | function blurImageData(imageData, radius) { 103 | const { height, width } = imageData; 104 | 105 | // comment before return 106 | return ( 107 | // within return expression 108 | Promise.resolve(imageData.data.buffer.slice(0)) 109 | // part of the resolve 110 | .then(bufferCopy => makeTransferable(bufferCopy)) 111 | // chain 112 | .then(transferable => promiseBlur(transferable, width, height, radius)) 113 | // more chaining comments 114 | .then(newBuffer => new Uint8ClampedArray(newBuffer)) 115 | .then(pixels => imageData.data.set(pixels)) 116 | .then(() => imageData) 117 | ); 118 | } 119 | 120 | function arrayDestructuring() { 121 | return b().then(([, destructuredArrayElement]) => { 122 | return destructuredArrayElement.d; 123 | }); 124 | } 125 | 126 | function doesSomethingWithAPromise() { 127 | promise.then(doSomething); 128 | } 129 | 130 | function returnsSomethingDone() { 131 | return promise.then(doSomething); 132 | } 133 | 134 | function returnsSomethingDone2(options) { 135 | return promise.then(options.doSomething); 136 | } 137 | 138 | function returnsSomethingDone3(options) { 139 | return getPromise(options).then(options.doSomething); 140 | } 141 | 142 | function returnsCallbackResult(options) { 143 | return start().getPromise(options).then(getCallback()); 144 | } 145 | 146 | function returnsArrayCallbackResult(options) { 147 | return start().getPromise[1](options)[2].then(getCallback()); 148 | } 149 | 150 | function returnsMultipleParams() { 151 | return b().then((x, y) => { 152 | return x.foo + y.foo; 153 | }); 154 | } 155 | 156 | const emptyArrow = () => {}; 157 | 158 | function returnUndefined() { 159 | return b().then(() => {}); 160 | } 161 | 162 | function returnUndefinedChained() { 163 | return b().then(() => {}).then(undefinedParam => { 164 | return c(undefinedParam); 165 | }); 166 | } 167 | 168 | function conflictingVariableName() { 169 | const c = 'first'; 170 | return b().then(c => { 171 | return c.d; 172 | }); 173 | } 174 | 175 | function conflictingVariableNames() { 176 | const c = 'first'; 177 | return b().then(c => { 178 | // second 179 | return c.second().then(c => { 180 | // third 181 | return c.third(); 182 | }); 183 | }); 184 | } 185 | 186 | function conflictingVariableNamesWithShadowParam() { 187 | const c = 'first'; 188 | return b().then(c => { 189 | // second 190 | return c.second().then(c => { 191 | // third 192 | return c.third(c => { 193 | c.other(); 194 | }); 195 | }); 196 | }); 197 | } 198 | 199 | function conflictingVariableNamesWithShadowDeclaration() { 200 | const c = 'first'; 201 | return b().then(c => { 202 | // second 203 | return c.second().then(c => { 204 | // third 205 | return c.third(() => { 206 | const c = get(); 207 | c.other(); 208 | }); 209 | }); 210 | }); 211 | } 212 | 213 | function thenTrueCatchFalse() { 214 | return b().then(c => true).catch(() => false); 215 | } 216 | 217 | const arrowNullError = val => 218 | a.b(val).then(() => null).catch(err => err.message); 219 | 220 | class SimpleClass { 221 | a() { 222 | return b().then(c => c && c.d).catch(err => err); 223 | } 224 | } 225 | 226 | class ExtendedClass extends Base { 227 | a(...args) { 228 | this.ready(true); 229 | return Promise.resolve(this.run(args)) 230 | .then(() => this.ok && this.ready(false)) 231 | .catch(() => this.ok && this.ready(false)); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /__testfixtures__/async-await.output.js: -------------------------------------------------------------------------------- 1 | async function a() { 2 | const c = await b(); 3 | return c.d; 4 | } 5 | 6 | async function thenFulfilledRejected() { 7 | try { 8 | const c = await b(); 9 | return c.d; 10 | } catch (error) { 11 | console.error(error); 12 | } 13 | } 14 | 15 | async function thenDotCatch() { 16 | try { 17 | const c = await b(); 18 | return c.d; 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | } 23 | 24 | async function embedded() { 25 | await first(); 26 | await second(); 27 | return third(); 28 | } 29 | 30 | async function promiseArrowShorthand() { 31 | const result = await asyncFunc(); 32 | return { result }; 33 | } 34 | 35 | const functionExpression = async function() { 36 | const result = await asyncFunc(); 37 | return result * 2; 38 | }; 39 | 40 | const functionExpressionCatch = async function() { 41 | try { 42 | const result = await asyncFunc(); 43 | return result * 2; 44 | } catch (error) { 45 | return console.error(error); 46 | } 47 | }; 48 | 49 | async function countUserVotes(userIds) { 50 | const users = await getUsers(userIds); 51 | return Promise.reduce(users, async (acc, user) => { 52 | const count = await user.getVoteCount(); 53 | return acc + count; 54 | }); 55 | } 56 | 57 | async function destructure(key) { 58 | const { [key]: result } = await asyncFunc(); 59 | return result * 3; 60 | } 61 | 62 | const makeRequestFunction = async function() { 63 | const data = await getJSON(); 64 | console.log(data); 65 | return "done"; 66 | }; 67 | 68 | const makeRequestArrowLong = async () => { 69 | const data = await getJSON(); 70 | console.log(data); 71 | return "done"; 72 | }; 73 | 74 | const makeRequest = async () => { 75 | const data = await getJSON(); 76 | console.log(data); 77 | return "done"; 78 | }; 79 | 80 | async function chainEventualThen() { 81 | const items = await Model.find().exec(); 82 | return items.map(item => item.thing); 83 | } 84 | 85 | app.get("/with-return", async function(req, res) { 86 | try { 87 | const recipeResponse = await requestPromise(generateBeefFreeRecipeURL()); 88 | const recipesList = JSON.parse(recipeResponse).results; 89 | const recipe = recipesList[0]; 90 | const responseText = `
${
 91 |       cowsay.say({
 92 |         text: recipe.title
 93 |       })
 94 |     }
`; 95 | 96 | res.send(responseText); 97 | } catch (error) { 98 | console.log(error); 99 | } 100 | }); 101 | 102 | async function blurImageData(imageData, radius) { 103 | const { height, width } = imageData; 104 | 105 | // comment before return 106 | // within return expression 107 | await Promise.resolve(imageData.data.buffer.slice(0)) 108 | // part of the resolve 109 | .then(bufferCopy => makeTransferable(bufferCopy)) 110 | // chain 111 | .then(transferable => promiseBlur(transferable, width, height, radius)) 112 | // more chaining comments 113 | .then(newBuffer => new Uint8ClampedArray(newBuffer)) 114 | .then(pixels => imageData.data.set(pixels)); 115 | 116 | return imageData; 117 | } 118 | 119 | async function arrayDestructuring() { 120 | const [, destructuredArrayElement] = await b(); 121 | return destructuredArrayElement.d; 122 | } 123 | 124 | function doesSomethingWithAPromise() { 125 | promise.then(doSomething); 126 | } 127 | 128 | async function returnsSomethingDone() { 129 | const promiseResult = await promise; 130 | return doSomething(promiseResult); 131 | } 132 | 133 | async function returnsSomethingDone2(options) { 134 | const promiseResult = await promise; 135 | return options.doSomething(promiseResult); 136 | } 137 | 138 | async function returnsSomethingDone3(options) { 139 | const getPromiseResult = await getPromise(options); 140 | return options.doSomething(getPromiseResult); 141 | } 142 | 143 | async function returnsCallbackResult(options) { 144 | const getPromiseResult = await start().getPromise(options); 145 | return getCallback()(getPromiseResult); 146 | } 147 | 148 | async function returnsArrayCallbackResult(options) { 149 | const getPromiseResult = await start().getPromise[1](options)[2]; 150 | return getCallback()(getPromiseResult); 151 | } 152 | 153 | async function returnsMultipleParams() { 154 | const [x, y] = await b(); 155 | return x.foo + y.foo; 156 | } 157 | 158 | const emptyArrow = () => {}; 159 | 160 | async function returnUndefined() { 161 | await b(); 162 | } 163 | 164 | async function returnUndefinedChained() { 165 | const undefinedParam = await b().then(() => {}); 166 | return c(undefinedParam); 167 | } 168 | 169 | async function conflictingVariableName() { 170 | const c = 'first'; 171 | const c2 = await b(); 172 | return c2.d; 173 | } 174 | 175 | async function conflictingVariableNames() { 176 | const c = 'first'; 177 | const c2 = await b(); 178 | 179 | // second 180 | const c3 = await c2.second(); 181 | 182 | // third 183 | return c3.third(); 184 | } 185 | 186 | async function conflictingVariableNamesWithShadowParam() { 187 | const c = 'first'; 188 | const c2 = await b(); 189 | 190 | // second 191 | const c3 = await c2.second(); 192 | 193 | // third 194 | return c3.third(c => { 195 | c.other(); 196 | }); 197 | } 198 | 199 | async function conflictingVariableNamesWithShadowDeclaration() { 200 | const c = 'first'; 201 | const c2 = await b(); 202 | 203 | // second 204 | const c3 = await c2.second(); 205 | 206 | // third 207 | return c3.third(() => { 208 | const c = get(); 209 | c.other(); 210 | }); 211 | } 212 | 213 | async function thenTrueCatchFalse() { 214 | try { 215 | const c = await b(); 216 | return true; 217 | } catch (error) { 218 | return false; 219 | } 220 | } 221 | 222 | const arrowNullError = async val => { 223 | try { 224 | await a.b(val); 225 | return null; 226 | } catch (err) { 227 | return err.message; 228 | } 229 | }; 230 | 231 | class SimpleClass { 232 | async a() { 233 | try { 234 | const c = await b(); 235 | return c && c.d; 236 | } catch (err) { 237 | return err; 238 | } 239 | } 240 | } 241 | 242 | class ExtendedClass extends Base { 243 | async a(...args) { 244 | this.ready(true); 245 | 246 | try { 247 | await Promise.resolve(this.run(args)); 248 | return this.ok && this.ready(false); 249 | } catch (error) { 250 | return this.ok && this.ready(false); 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /__testfixtures__/await-promise-chain.input.js: -------------------------------------------------------------------------------- 1 | async function thenChain() { 2 | console.log('before'); 3 | 4 | // comment about b 5 | const e = await b().then(c => { 6 | // comment before using c 7 | return c.d(); 8 | }); 9 | 10 | return 'end with ' + e; 11 | } 12 | 13 | function thenChainNotAsync() { 14 | return b().then(c => { 15 | return c.d(); 16 | }).then(e => { 17 | return 'end with ' + e; 18 | }); 19 | } 20 | 21 | async function longThenChain() { 22 | const end = await b().then(c => { 23 | c.readyForD = true; 24 | return c.d(); 25 | }).then(e => { 26 | // comment about e 27 | return e.f(); 28 | }); 29 | 30 | return 'end with ' + end; 31 | } 32 | 33 | async function thenCatchChain() { 34 | console.log('before'); 35 | 36 | // comment about b 37 | const e = await b().then(c => { 38 | // comment before using c 39 | return c.d(); 40 | }).catch(error => { 41 | console.error(error); 42 | }); 43 | 44 | return 'end with ' + e; 45 | } 46 | 47 | async function returnUndefinedChained() { 48 | const undefinedParam = await b().then(() => {}); 49 | return c(undefinedParam); 50 | } 51 | 52 | async function returnAssignmentChained() { 53 | await b().then(p => (this.c = p.c)); 54 | return d(this.c); 55 | } 56 | 57 | async function awaitExpression() { 58 | await Factory.create('coach', {}) 59 | .then(coach => { 60 | return client.put('/coach/' + coach.id); 61 | }); 62 | } 63 | 64 | async function returnAwaitExpression() { 65 | return await Factory.create('coach', {}) 66 | .then(coach => { 67 | return client.put('/coach/' + coach.id); 68 | }); 69 | } 70 | 71 | async function returnAwaitExpressionSync() { 72 | return await Factory.create('coach', {}) 73 | .then(coach => { 74 | sync(coach); 75 | }); 76 | } 77 | 78 | async function conflictingVariableNamesWithShadowDeclaration() { 79 | const c = 'first'; 80 | return await b().then(c => { 81 | // second 82 | return c.second().then(c => { 83 | // third 84 | return c.third(() => { 85 | const c = get(); 86 | c.other(); 87 | }); 88 | }); 89 | }); 90 | } 91 | 92 | async function conflictingVariableNamesWithShadowDestructuredDeclaration() { 93 | const [,c] = [1, 'first']; 94 | return await b().then(c => { 95 | // second 96 | return c.second().then(c => { 97 | // third 98 | return c.third(() => { 99 | const c = get(); 100 | c.other(); 101 | }); 102 | }); 103 | }); 104 | } 105 | 106 | async function conflictingSpreadDeclaration() { 107 | console.log('before'); 108 | 109 | // comment about b 110 | const e = await b().spread((req, e) => { 111 | // comment before using e 112 | return e.d(req); 113 | }); 114 | 115 | return 'end with ' + e; 116 | } 117 | 118 | async function conflictingDestructuredSpreadDeclaration() { 119 | console.log('before'); 120 | 121 | // comment about b 122 | const [req, e] = await b().spread((req, e) => { 123 | // comment before using e 124 | return e.d(req); 125 | }); 126 | 127 | return 'end with ' + e + req.id(); 128 | } 129 | 130 | async function awaitConditionalReturn() { 131 | return await Factory.create('coach', {}) 132 | .then(coach => { 133 | if (!coach.good) { 134 | return; 135 | } 136 | return client.put('/coach/' + coach.id); 137 | }); 138 | } 139 | 140 | async function awaitSync() { 141 | const self = this; 142 | 143 | await Factory.create('patient') 144 | .then(function (patient) { 145 | self.patient = patient; 146 | }) 147 | .then(async function () { 148 | const device = await Factory.create('mobile_device', {_user: self.patient}); 149 | self.device = device; 150 | }); 151 | 152 | pushEvents.configure(); 153 | } -------------------------------------------------------------------------------- /__testfixtures__/await-promise-chain.output.js: -------------------------------------------------------------------------------- 1 | async function thenChain() { 2 | console.log('before'); 3 | 4 | // comment about b 5 | const c = await b(); 6 | 7 | // comment before using c 8 | const e = await c.d(); 9 | 10 | return 'end with ' + e; 11 | } 12 | 13 | function thenChainNotAsync() { 14 | return b().then(c => { 15 | return c.d(); 16 | }).then(e => { 17 | return 'end with ' + e; 18 | }); 19 | } 20 | 21 | async function longThenChain() { 22 | const c = await b(); 23 | c.readyForD = true; 24 | const e = await c.d(); 25 | // comment about e 26 | const end = await e.f(); 27 | 28 | return 'end with ' + end; 29 | } 30 | 31 | async function thenCatchChain() { 32 | console.log('before'); 33 | 34 | try { 35 | // comment about b 36 | const c = await b(); 37 | 38 | // comment before using c 39 | const e = await c.d(); 40 | } catch (error) { 41 | console.error(error); 42 | } 43 | 44 | return 'end with ' + e; 45 | } 46 | 47 | async function returnUndefinedChained() { 48 | const undefinedParam = await b().then(() => {}); 49 | return c(undefinedParam); 50 | } 51 | 52 | async function returnAssignmentChained() { 53 | const p = await b(); 54 | this.c = p.c; 55 | return d(this.c); 56 | } 57 | 58 | async function awaitExpression() { 59 | const coach = await Factory.create('coach', {}); 60 | await client.put('/coach/' + coach.id); 61 | } 62 | 63 | async function returnAwaitExpression() { 64 | const coach = await Factory.create('coach', {}); 65 | return await client.put('/coach/' + coach.id); 66 | } 67 | 68 | async function returnAwaitExpressionSync() { 69 | const coach = await Factory.create('coach', {}); 70 | sync(coach); 71 | } 72 | 73 | async function conflictingVariableNamesWithShadowDeclaration() { 74 | const c = 'first'; 75 | const c2 = await b(); 76 | 77 | // second 78 | const c3 = await c2.second(); 79 | 80 | // third 81 | return await c3.third(() => { 82 | const c = get(); 83 | c.other(); 84 | }); 85 | } 86 | 87 | async function conflictingVariableNamesWithShadowDestructuredDeclaration() { 88 | const [,c] = [1, 'first']; 89 | const c2 = await b(); 90 | 91 | // second 92 | const c3 = await c2.second(); 93 | 94 | // third 95 | return await c3.third(() => { 96 | const c = get(); 97 | c.other(); 98 | }); 99 | } 100 | 101 | async function conflictingSpreadDeclaration() { 102 | console.log('before'); 103 | 104 | // comment about b 105 | const [req, e2] = await b(); 106 | 107 | // comment before using e 108 | const e = await e2.d(req); 109 | 110 | return 'end with ' + e; 111 | } 112 | 113 | async function conflictingDestructuredSpreadDeclaration() { 114 | console.log('before'); 115 | 116 | // comment about b 117 | const [req2, e2] = await b(); 118 | 119 | // comment before using e 120 | const [req, e] = await e2.d(req2); 121 | 122 | return 'end with ' + e + req.id(); 123 | } 124 | 125 | async function awaitConditionalReturn() { 126 | const coach = await Factory.create('coach', {}); 127 | if (!coach.good) { 128 | return; 129 | } 130 | return await client.put('/coach/' + coach.id); 131 | } 132 | 133 | async function awaitSync() { 134 | const self = this; 135 | 136 | const patient = await Factory.create('patient'); 137 | self.patient = patient; 138 | const device = await Factory.create('mobile_device', {_user: self.patient}); 139 | self.device = device; 140 | 141 | pushEvents.configure(); 142 | } -------------------------------------------------------------------------------- /__tests__/async-await-test.js: -------------------------------------------------------------------------------- 1 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 2 | const {defineTransformTestFromFunctions} = require('../lib/test-utils'); 3 | const transform = require('../async-await'); 4 | 5 | function defineTestFromFunctions(input, output, name) { 6 | defineTransformTestFromFunctions(transform, input, output, name); 7 | } 8 | 9 | describe('async-await', () => { 10 | describe('fixtures', function() { 11 | defineTest(__dirname, 'async-await'); 12 | }); 13 | 14 | describe('simple then', function() { 15 | defineTestFromFunctions( 16 | () => { 17 | function a() { 18 | return b().then(c => { 19 | return c.d; 20 | }); 21 | } 22 | }, 23 | () => { 24 | async function a() { 25 | const c = await b(); 26 | return c.d; 27 | } 28 | } 29 | ); 30 | }); 31 | 32 | describe('should use let instead of const if parameter is reassigned', function() { 33 | defineTestFromFunctions( 34 | () => { 35 | function a() { 36 | return b().then(c => { 37 | c = c.child; 38 | return c.d; 39 | }); 40 | } 41 | }, 42 | () => { 43 | async function a() { 44 | let c = await b(); 45 | c = c.child; 46 | return c.d; 47 | } 48 | } 49 | ); 50 | }); 51 | 52 | describe('should use let instead of const if parameter is updated', function() { 53 | defineTestFromFunctions( 54 | () => { 55 | function a() { 56 | return b().then(c => { 57 | c++; 58 | return c; 59 | }); 60 | } 61 | }, 62 | () => { 63 | async function a() { 64 | let c = await b(); 65 | c++; 66 | return c; 67 | } 68 | } 69 | ); 70 | }); 71 | 72 | describe('then not returned should not transform', function() { 73 | // transforming to async/await would change the behavior of the function 74 | defineTestFromFunctions( 75 | () => { 76 | function a() { 77 | b().then(c => { 78 | return c.d; 79 | }); 80 | } 81 | }, 82 | () => { 83 | function a() { 84 | b().then(c => { 85 | return c.d; 86 | }); 87 | } 88 | } 89 | ); 90 | }); 91 | 92 | describe('then with rejection handler', function() { 93 | defineTestFromFunctions( 94 | () => { 95 | function thenFulfilledRejected() { 96 | return b().then( 97 | c => { 98 | return c.d; 99 | }, 100 | error => { 101 | console.error(error); 102 | } 103 | ); 104 | } 105 | }, 106 | () => { 107 | async function thenFulfilledRejected() { 108 | try { 109 | const c = await b(); 110 | return c.d; 111 | } catch (error) { 112 | console.error(error); 113 | } 114 | } 115 | } 116 | ); 117 | }); 118 | 119 | describe('await result should avoid param name conflict', function() { 120 | defineTestFromFunctions( 121 | () => { 122 | function paramNameConflict(category) { 123 | return category.get().then(function(category) { 124 | return category.name; 125 | }); 126 | } 127 | }, 128 | () => { 129 | async function paramNameConflict(category) { 130 | const category2 = await category.get(); 131 | return category2.name; 132 | } 133 | } 134 | ); 135 | }); 136 | 137 | describe('await result should avoid variable name conflict', function() { 138 | defineTestFromFunctions( 139 | () => { 140 | function paramNameConflict() { 141 | const category = new Category(); 142 | return category.get().then(function(category) { 143 | return category.name; 144 | }); 145 | } 146 | }, 147 | () => { 148 | async function paramNameConflict() { 149 | const category = new Category(); 150 | const category2 = await category.get(); 151 | return category2.name; 152 | } 153 | } 154 | ); 155 | }); 156 | 157 | describe('await result should avoid variable name conflict inside an anonymous function', function() { 158 | defineTestFromFunctions( 159 | () => { 160 | it('test', function a() { 161 | const category = new Category(); 162 | return category.get().then(function(category) { 163 | return category.name; 164 | }); 165 | }); 166 | }, 167 | () => { 168 | it('test', async function a() { 169 | const category = new Category(); 170 | const category2 = await category.get(); 171 | return category2.name; 172 | }); 173 | } 174 | ); 175 | }); 176 | 177 | describe('await result should avoid variable name conflict for an arrow function inside an anonymous function', function() { 178 | defineTestFromFunctions( 179 | ` 180 | it('test', function() { 181 | const plan = new Plan(); 182 | return plan.save().then(() => { 183 | return Factory.create('plan'); 184 | }) 185 | .then(plan => { 186 | return Plan.update().then(() => { 187 | return Plan.findById(plan).exec(); 188 | }) 189 | .then(plan => { 190 | expect(plan.adherence_updated_at).to.equalTime(new Date()); 191 | }); 192 | }); 193 | }); 194 | `, 195 | ` 196 | it('test', async function() { 197 | const plan = new Plan(); 198 | 199 | const plan2 = await plan.save().then(() => { 200 | return Factory.create('plan'); 201 | }); 202 | 203 | const plan3 = await Plan.update().then(() => { 204 | return Plan.findById(plan2).exec(); 205 | }); 206 | 207 | expect(plan3.adherence_updated_at).to.equalTime(new Date()); 208 | }); 209 | ` 210 | ); 211 | }); 212 | 213 | // not supported yet 214 | describe.skip('await result should avoid variable name conflict for an anonymous function inside an arrow function', function() { 215 | defineTestFromFunctions( 216 | () => { 217 | it('test', () => { 218 | const plan = new Plan(); 219 | return plan 220 | .save() 221 | .then(() => { 222 | return Factory.create('plan'); 223 | }) 224 | .then(function(plan) { 225 | return Plan.update() 226 | .then(() => { 227 | return Plan.findById(plan).exec(); 228 | }) 229 | .then(function(plan) { 230 | expect(plan.adherence_updated_at).to.equalTime(new Date()); 231 | }); 232 | }); 233 | }); 234 | }, 235 | () => { 236 | it('test', async () => { 237 | const plan = new Plan(); 238 | 239 | const plan2 = await plan.save().then(() => { 240 | return Factory.create('plan'); 241 | }); 242 | 243 | const plan3 = await Plan.update().then(() => { 244 | return Plan.findById(plan2).exec(); 245 | }); 246 | 247 | expect(plan3.adherence_updated_at).to.equalTime(new Date()); 248 | }); 249 | } 250 | ); 251 | }); 252 | 253 | describe('await result from an array destructured param should avoid conflict', function() { 254 | defineTestFromFunctions( 255 | () => { 256 | function a() { 257 | const entry = getEntries(); 258 | return b(entry).then(([entry]) => { 259 | return c.d(entry); 260 | }); 261 | } 262 | }, 263 | () => { 264 | async function a() { 265 | const entry = getEntries(); 266 | const [entry2] = await b(entry); 267 | return c.d(entry2); 268 | } 269 | } 270 | ); 271 | }); 272 | 273 | describe('await result from an array destructured param should avoid another destructured conflict', function() { 274 | defineTestFromFunctions( 275 | () => { 276 | function a() { 277 | return getEntries().then(([entry]) => { 278 | return b(entry).then(([entry]) => { 279 | return c.d(entry); 280 | }); 281 | }); 282 | } 283 | }, 284 | () => { 285 | async function a() { 286 | const [entry] = await getEntries(); 287 | const [entry2] = await b(entry); 288 | return c.d(entry2); 289 | } 290 | } 291 | ); 292 | }); 293 | 294 | describe('await result should avoid unpacked param name conflict', function() { 295 | defineTestFromFunctions( 296 | ` 297 | function paramNameConflict({category}) { 298 | return category.get().then(function(category) { 299 | return category.name; 300 | }); 301 | } 302 | `, 303 | ` 304 | async function paramNameConflict({category}) { 305 | const category2 = await category.get(); 306 | return category2.name; 307 | } 308 | ` 309 | ); 310 | }); 311 | 312 | describe('await result should avoid unpacked nested param name conflict', function() { 313 | defineTestFromFunctions( 314 | ` 315 | function paramNameConflict({results: {category}}) { 316 | return category.get().then(function(category) { 317 | return category.name; 318 | }); 319 | } 320 | `, 321 | ` 322 | async function paramNameConflict({results: {category}}) { 323 | const category2 = await category.get(); 324 | return category2.name; 325 | } 326 | ` 327 | ); 328 | }); 329 | 330 | describe('await result should avoid unpacked nested renamed param name conflict', function() { 331 | defineTestFromFunctions( 332 | ` 333 | function paramNameConflict({results: {original: category}}) { 334 | return category.get().then(function(category) { 335 | return category.name; 336 | }); 337 | } 338 | `, 339 | ` 340 | async function paramNameConflict({results: {original: category}}) { 341 | const category2 = await category.get(); 342 | return category2.name; 343 | } 344 | ` 345 | ); 346 | }); 347 | 348 | describe('returned rejection handler as identifier', function() { 349 | defineTestFromFunctions( 350 | () => { 351 | function thenFulfilledRejected() { 352 | return b().then(c => { 353 | return c.d; 354 | }, callback); 355 | } 356 | }, 357 | () => { 358 | async function thenFulfilledRejected() { 359 | try { 360 | const c = await b(); 361 | return c.d; 362 | } catch (error) { 363 | return callback(error); 364 | } 365 | } 366 | } 367 | ); 368 | }); 369 | 370 | describe('block statements before try', function() { 371 | defineTestFromFunctions( 372 | () => { 373 | function blockBefore() { 374 | const pre = 1; 375 | return a().then(something, callback); 376 | } 377 | }, 378 | () => { 379 | async function blockBefore() { 380 | const pre = 1; 381 | 382 | try { 383 | const aResult = await a(); 384 | return something(aResult); 385 | } catch (error) { 386 | return callback(error); 387 | } 388 | } 389 | } 390 | ); 391 | }); 392 | 393 | describe('non-returned rejection handler should not transform', function() { 394 | defineTestFromFunctions( 395 | () => { 396 | function thenFulfilledRejected() { 397 | b().then(c => { 398 | return c.d; 399 | }, callback); 400 | } 401 | }, 402 | () => { 403 | function thenFulfilledRejected() { 404 | b().then(c => { 405 | return c.d; 406 | }, callback); 407 | } 408 | } 409 | ); 410 | }); 411 | 412 | describe('two unchained promises', function() { 413 | defineTestFromFunctions( 414 | () => { 415 | class TwoPromises { 416 | a() { 417 | b().then(() => f(1)); 418 | c().then(() => f(2)); 419 | } 420 | } 421 | }, 422 | () => { 423 | class TwoPromises { 424 | a() { 425 | b().then(() => f(1)); 426 | c().then(() => f(2)); 427 | } 428 | } 429 | } 430 | ); 431 | }); 432 | 433 | describe('an unchained followed by a chained promise', function() { 434 | defineTestFromFunctions( 435 | () => { 436 | class TwoPromises { 437 | a() { 438 | b().then(() => f(1)); 439 | return c().then(() => f(2)); 440 | } 441 | } 442 | }, 443 | () => { 444 | class TwoPromises { 445 | async a() { 446 | b().then(() => f(1)); 447 | await c(); 448 | return f(2); 449 | } 450 | } 451 | } 452 | ); 453 | }); 454 | 455 | describe('statement after then', function() { 456 | defineTestFromFunctions( 457 | () => { 458 | function a() { 459 | b().then(() => f(1)); 460 | return f(2); 461 | } 462 | }, 463 | () => { 464 | function a() { 465 | b().then(() => f(1)); 466 | return f(2); 467 | } 468 | } 469 | ); 470 | }); 471 | 472 | describe('no return expression', function() { 473 | defineTestFromFunctions( 474 | () => { 475 | function countUserVotes(userIds) { 476 | return getUsers(userIds).then(users => { 477 | return Promise.reduce(users, (acc, user) => { 478 | return user.getVoteCount().then(count => acc + count); 479 | }); 480 | }); 481 | } 482 | }, 483 | () => { 484 | async function countUserVotes(userIds) { 485 | const users = await getUsers(userIds); 486 | return Promise.reduce(users, async (acc, user) => { 487 | const count = await user.getVoteCount(); 488 | return acc + count; 489 | }); 490 | } 491 | } 492 | ); 493 | }); 494 | 495 | describe('return undefined chained', function() { 496 | defineTestFromFunctions( 497 | () => { 498 | function returnUndefinedChained() { 499 | return b() 500 | .then(() => {}) 501 | .then(undefinedParam => { 502 | return c(undefinedParam); 503 | }); 504 | } 505 | }, 506 | () => { 507 | async function returnUndefinedChained() { 508 | const undefinedParam = await b().then(() => {}); 509 | return c(undefinedParam); 510 | } 511 | } 512 | ); 513 | }); 514 | 515 | describe('then with shadow variable declaration', function() { 516 | defineTestFromFunctions( 517 | () => { 518 | function a() { 519 | const entry = 1; 520 | return b(entry).then(c => { 521 | const entry = 2; 522 | return c.d(entry); 523 | }); 524 | } 525 | }, 526 | () => { 527 | async function a() { 528 | const entry = 1; 529 | const c = await b(entry); 530 | const entry2 = 2; 531 | return c.d(entry2); 532 | } 533 | } 534 | ); 535 | }); 536 | 537 | describe('then with shadow variable declaration and conflict for rename', function() { 538 | defineTestFromFunctions( 539 | () => { 540 | function a() { 541 | const entry = 1, 542 | entry2 = 1.2; 543 | return b(entry, entry2).then(c => { 544 | const entry = 2; 545 | return c.d(entry); 546 | }); 547 | } 548 | }, 549 | () => { 550 | async function a() { 551 | const entry = 1, 552 | entry2 = 1.2; 553 | const c = await b(entry, entry2); 554 | const entry3 = 2; 555 | return c.d(entry3); 556 | } 557 | } 558 | ); 559 | }); 560 | 561 | describe('then with shadow function declaration', function() { 562 | defineTestFromFunctions( 563 | ` 564 | function a() { 565 | function getEntry() { 566 | return 1; 567 | } 568 | return b(getEntry()).then(c => { 569 | function getEntry() { 570 | return 2; 571 | } 572 | return c.d(getEntry()); 573 | }); 574 | } 575 | `, 576 | ` 577 | async function a() { 578 | function getEntry() { 579 | return 1; 580 | } 581 | const c = await b(getEntry()); 582 | function getEntry2() { 583 | return 2; 584 | } 585 | return c.d(getEntry2()); 586 | } 587 | ` 588 | ); 589 | }); 590 | 591 | describe('rejection handler defined but fulfilled handler undefined', function() { 592 | defineTestFromFunctions( 593 | () => { 594 | function undefinedFulfilled() { 595 | return a().then(undefined, callback); 596 | } 597 | }, 598 | () => { 599 | async function undefinedFulfilled() { 600 | try { 601 | await a(); 602 | } catch (error) { 603 | return callback(error); 604 | } 605 | } 606 | } 607 | ); 608 | }); 609 | 610 | describe('spread to fulfilled handler that is an arrow function', function() { 611 | defineTestFromFunctions( 612 | () => { 613 | function spread() { 614 | return b().spread((c, d) => { 615 | return c(d).e; 616 | }); 617 | } 618 | }, 619 | () => { 620 | async function spread() { 621 | const [c, d] = await b(); 622 | return c(d).e; 623 | } 624 | } 625 | ); 626 | }); 627 | 628 | describe('spread of single param to fulfilled handler that is an arrow function', function() { 629 | defineTestFromFunctions( 630 | () => { 631 | function spread() { 632 | return b().spread(c => { 633 | return c().d; 634 | }); 635 | } 636 | }, 637 | () => { 638 | async function spread() { 639 | const [c] = await b(); 640 | return c().d; 641 | } 642 | } 643 | ); 644 | }); 645 | 646 | describe('spread to fulfilled handler that is an identifier', function() { 647 | defineTestFromFunctions( 648 | () => { 649 | function spread() { 650 | return b().spread(doSomething); 651 | } 652 | }, 653 | () => { 654 | async function spread() { 655 | const bResult = await b(); 656 | return doSomething(...bResult); 657 | } 658 | } 659 | ); 660 | }); 661 | }); 662 | -------------------------------------------------------------------------------- /__tests__/await-promise-chain-test.js: -------------------------------------------------------------------------------- 1 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 2 | const {defineTransformTestFromFunctions} = require('../lib/test-utils'); 3 | const transform = require('../await-promise-chain'); 4 | 5 | function defineTestFromFunctions(input, output, name) { 6 | defineTransformTestFromFunctions(transform, input, output, name); 7 | } 8 | 9 | describe('await-promise-chain', () => { 10 | describe('fixtures', function() { 11 | defineTest(__dirname, 'await-promise-chain'); 12 | }); 13 | 14 | describe('simple chain', function() { 15 | defineTestFromFunctions( 16 | () => { 17 | async function thenChain() { 18 | console.log('before'); 19 | 20 | const e = await b().then(c => { 21 | return c.d(); 22 | }); 23 | 24 | return 'end with ' + e; 25 | } 26 | }, 27 | () => { 28 | async function thenChain() { 29 | console.log('before'); 30 | 31 | const c = await b(); 32 | 33 | const e = await c.d(); 34 | 35 | return 'end with ' + e; 36 | } 37 | } 38 | ); 39 | }); 40 | 41 | describe('return assignment chained', function() { 42 | defineTestFromFunctions( 43 | () => { 44 | async function returnAssignmentChained() { 45 | await b().then(p => (this.c = p.c)); 46 | return d(this.c); 47 | } 48 | }, 49 | () => { 50 | async function returnAssignmentChained() { 51 | const p = await b(); 52 | this.c = p.c; 53 | return d(this.c); 54 | } 55 | } 56 | ); 57 | }); 58 | 59 | describe.skip('transform variable declaration with conditional return', function() { 60 | defineTestFromFunctions( 61 | () => { 62 | async function thenChain() { 63 | console.log('before'); 64 | 65 | const e = await b().then(c => { 66 | if (c) { 67 | return c.d(); 68 | } 69 | }); 70 | 71 | return 'end with ' + e; 72 | } 73 | }, 74 | () => { 75 | async function thenChain() { 76 | console.log('before'); 77 | 78 | const c = await b(); 79 | 80 | let e; 81 | if (c) { 82 | e = await c.d(); 83 | } 84 | 85 | return 'end with ' + e; 86 | } 87 | } 88 | ); 89 | }); 90 | 91 | describe('should not transform variable declaration with conditional return', function() { 92 | defineTestFromFunctions( 93 | () => { 94 | async function thenChain() { 95 | console.log('before'); 96 | 97 | const e = await b().then(c => { 98 | if (c) { 99 | return c.d(); 100 | } 101 | }); 102 | 103 | return 'end with ' + e; 104 | } 105 | }, 106 | () => { 107 | async function thenChain() { 108 | console.log('before'); 109 | 110 | const e = await b().then(c => { 111 | if (c) { 112 | return c.d(); 113 | } 114 | }); 115 | 116 | return 'end with ' + e; 117 | } 118 | } 119 | ); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /async-await.js: -------------------------------------------------------------------------------- 1 | const utils = require('./lib/utils'); 2 | 3 | const DEFAULT_ERROR_NODE = { 4 | type: 'Identifier', 5 | name: 'error', 6 | optional: false, 7 | typeAnnotation: null 8 | }; 9 | 10 | module.exports = function transformer(file, api) { 11 | const j = api.jscodeshift; 12 | const root = j(file.source); 13 | 14 | const funcReturnsPromise = p => { 15 | const body = p.node.body.body; 16 | const last = body[body.length - 1]; 17 | if (!last || last.type !== 'ReturnStatement') { 18 | return false; 19 | } 20 | return utils.isPromiseCall(last.argument); 21 | }; 22 | 23 | const arrowReturnsPromise = p => { 24 | const node = p.node; 25 | 26 | if (node.body.type === 'BlockStatement') { 27 | const body = node.body.body; 28 | const last = body[body.length - 1]; 29 | if (!last || last.type !== 'ReturnStatement') { 30 | return false; 31 | } 32 | return utils.isPromiseCall(last.argument); 33 | } 34 | 35 | return utils.isPromiseCall(node.body); 36 | }; 37 | 38 | const getRestFromCallBack = (p, callBack, lastExp, resultIdentifierName) => { 39 | let rest; 40 | if (!callBack.body) { 41 | const callBackCall = j.callStatement(callBack, [ 42 | lastExp.argument.callee.property.name === 'spread' 43 | ? j.spreadElement(j.identifier(resultIdentifierName)) 44 | : j.identifier(resultIdentifierName) 45 | ]); 46 | if (callBack.type === 'Identifier' && callBack.name === 'undefined') { 47 | // "return a().then(undefined)" becomes "await a()" 48 | rest = []; 49 | } else if (lastExp.type === 'ReturnStatement') { 50 | // "return promise.then(doSomething)" becomes "return doSomething(promiseResult)" 51 | rest = [j.returnStatement(callBackCall.expression)]; 52 | } else { 53 | // "promise.then(doSomething)" becomes "doSomething(promiseResult)" 54 | rest = [callBackCall]; 55 | } 56 | } else if (callBack.body.type === 'BlockStatement') { 57 | rest = callBack.body.body; 58 | utils.resolveNameConflicts(j, p, callBack.body); 59 | } else { 60 | rest = [j.returnStatement(callBack.body)]; 61 | } 62 | return rest; 63 | }; 64 | 65 | function getCalleeName(thenCalleeObject) { 66 | let currentNode = thenCalleeObject; 67 | while (currentNode && !currentNode.name) { 68 | if (currentNode.property && currentNode.property.type === 'Identifier') { 69 | currentNode = currentNode.property; 70 | } else { 71 | currentNode = currentNode.callee || currentNode.object; 72 | } 73 | } 74 | // if we failed to get a name from iterating on callee/property, fallback to using 'promise' 75 | return currentNode ? currentNode.name : 'promise'; 76 | } 77 | 78 | const transformFunction = p => { 79 | const node = p.node; 80 | 81 | let bodyStatements; 82 | if (node.body.type === 'BlockStatement') { 83 | bodyStatements = node.body.body; 84 | } else { 85 | bodyStatements = [node.body]; 86 | } 87 | 88 | if (!bodyStatements) { 89 | // eslint-disable-next-line no-console 90 | console.log('no body', node.type, node.loc); 91 | return; 92 | } 93 | // Transform return 94 | const lastExp = bodyStatements[bodyStatements.length - 1]; 95 | 96 | // if lastExp is a return, use the argument 97 | const callExp = lastExp.expression || lastExp.argument || lastExp; 98 | if (!callExp) { 99 | // the lack of any statements in fulfilled handler is unusual but 100 | // might be intentional 101 | 102 | // eslint-disable-next-line no-console 103 | console.log('no return expression', node.type, lastExp.loc); 104 | return; 105 | } 106 | 107 | // Set function to async 108 | node.async = true; 109 | 110 | let {errorCallBack, callBack, thenCalleeObject} = utils.parseCallExpression( 111 | callExp 112 | ); 113 | const calleeName = getCalleeName(thenCalleeObject); 114 | // TODO: we should ensure the generated resultIdentifierName is unique because it might conflict with the name of another identifier 115 | const resultIdentifierName = calleeName + 'Result'; 116 | 117 | // Create await statement 118 | let awaition; 119 | if (callBack.params) { 120 | if (callBack.params.length > 0) { 121 | utils.resolveParamNameConflicts(j, p, callBack); 122 | const kind = utils.getParamsDeclarationKind(j, p, callBack); 123 | awaition = utils.genAwaitionDeclarator( 124 | j, 125 | callExp, 126 | callBack, 127 | kind, 128 | callBack.params, 129 | thenCalleeObject 130 | ); 131 | } else { 132 | awaition = j.expressionStatement(j.awaitExpression(thenCalleeObject)); 133 | } 134 | } else if ( 135 | callBack.type === 'Identifier' && 136 | callBack.name === 'undefined' 137 | ) { 138 | awaition = j.expressionStatement(j.awaitExpression(thenCalleeObject)); 139 | } else { 140 | // no params (and no body), not an inline function, so we can't simply use the body of the callee (?) 141 | awaition = utils.genAwaitionDeclarator( 142 | j, 143 | callExp, 144 | callBack, 145 | 'const', 146 | [j.identifier(resultIdentifierName)], 147 | thenCalleeObject 148 | ); 149 | } 150 | 151 | let leadingComments = awaition.leadingComments || []; 152 | if (callExp.leadingComments && callExp.leadingComments[0]) { 153 | // preserve any comments from the call expression 154 | leadingComments = callExp.leadingComments.concat(leadingComments); 155 | } 156 | if ( 157 | callExp !== lastExp && 158 | lastExp && 159 | lastExp.leadingComments && 160 | lastExp.leadingComments[0] 161 | ) { 162 | // preserve any comments from the last statement (generally the return expression) 163 | leadingComments = lastExp.leadingComments.concat(leadingComments); 164 | } 165 | awaition.comments = leadingComments; 166 | 167 | const rest = getRestFromCallBack( 168 | p, 169 | callBack, 170 | lastExp, 171 | resultIdentifierName 172 | ); 173 | 174 | // Replace the function's body with the new content 175 | const tryStatements = [awaition, ...rest]; 176 | 177 | const errorParam = 178 | errorCallBack && 179 | ((errorCallBack.params && errorCallBack.params[0]) || DEFAULT_ERROR_NODE); 180 | p.node.body = j.blockStatement( 181 | errorCallBack 182 | ? [ 183 | ...bodyStatements.slice(0, bodyStatements.length - 1), 184 | j.tryStatement( 185 | j.blockStatement(tryStatements), 186 | j.catchClause( 187 | errorParam, 188 | null, 189 | j.blockStatement( 190 | getRestFromCallBack( 191 | p, 192 | errorCallBack, 193 | lastExp, 194 | errorParam.name 195 | ) 196 | ) 197 | ) 198 | ) 199 | ] 200 | : [ 201 | ...bodyStatements.slice(0, bodyStatements.length - 1), 202 | ...tryStatements 203 | ] 204 | ); 205 | 206 | return p.node; 207 | }; 208 | 209 | const replaceType = (type, filterer = funcReturnsPromise) => { 210 | // Loop until all promises are gone or no transforms are possible 211 | let somethingTransformed = false; 212 | let iterations = 0; 213 | const iterationsLimit = 256; 214 | do { 215 | iterations++; 216 | const paths = root.find(type).filter(filterer); 217 | if (paths.size() === 0) { 218 | break; 219 | } 220 | 221 | somethingTransformed = false; 222 | paths.forEach(path => { 223 | if (transformFunction(path)) { 224 | somethingTransformed = true; 225 | } 226 | }); 227 | } while (somethingTransformed && iterations < iterationsLimit); 228 | }; 229 | 230 | replaceType(j.FunctionExpression); 231 | 232 | replaceType(j.FunctionDeclaration); 233 | 234 | replaceType(j.ArrowFunctionExpression, arrowReturnsPromise); 235 | 236 | // TODO: cover more async/await cases 237 | // TODO: cover .then().finally() 238 | 239 | return root.toSource(); 240 | }; 241 | -------------------------------------------------------------------------------- /await-promise-chain.js: -------------------------------------------------------------------------------- 1 | const utils = require('./lib/utils'); 2 | 3 | module.exports = function transformer(file, api) { 4 | const j = api.jscodeshift; 5 | const root = j(file.source); 6 | 7 | /** 8 | * Looks at each body statement of the node and returns the body statement and 9 | * inner await expression if the await is on a nested promise with a .then callback 10 | * @param p The path to check 11 | * @return {{awaitExpression: StatementTypes.expression, bodyStatement}} 12 | */ 13 | const containsAwaitOnPromise = p => { 14 | for (let bodyStatement of p.node.body) { 15 | let awaitExpression; 16 | if ( 17 | bodyStatement.type === 'ReturnStatement' && 18 | bodyStatement.argument && 19 | bodyStatement.argument.type === 'AwaitExpression' 20 | ) { 21 | awaitExpression = bodyStatement.argument; 22 | if (utils.isPromiseCall(awaitExpression.argument)) { 23 | return {bodyStatement, awaitExpression}; 24 | } 25 | } else if ( 26 | bodyStatement.type === 'ExpressionStatement' && 27 | bodyStatement.expression.type === 'AwaitExpression' 28 | ) { 29 | awaitExpression = bodyStatement.expression; 30 | if (utils.isPromiseCall(awaitExpression.argument)) { 31 | return {bodyStatement, awaitExpression}; 32 | } 33 | } else if (bodyStatement.type === 'AwaitExpression') { 34 | awaitExpression = bodyStatement; 35 | if (utils.isPromiseCall(awaitExpression.argument)) { 36 | return {bodyStatement, awaitExpression}; 37 | } 38 | } else if (bodyStatement.type === 'VariableDeclaration') { 39 | for (let declarator of bodyStatement.declarations) { 40 | if (declarator.init && declarator.init.type === 'AwaitExpression') { 41 | if (utils.isPromiseCall(declarator.init.argument)) { 42 | return { 43 | bodyStatement, 44 | awaitExpression: declarator.init 45 | }; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }; 52 | 53 | function statementsContainReturnRecursive(statements) { 54 | return statements.some( 55 | statement => 56 | statement.type === 'ReturnStatement' || 57 | (statement.type === 'IfStatement' && containsReturnRecursive(statement)) 58 | ); 59 | } 60 | 61 | function containsReturnRecursive(statement) { 62 | return !!( 63 | statementsContainReturnRecursive(statement.consequent.body) || 64 | (statement.alternate && statementsContainReturnRecursive(statement.alternate.body)) 65 | ); 66 | } 67 | 68 | function containsConditionalReturn(callbackStatements) { 69 | const lastStatement = 70 | callbackStatements.length > 0 && 71 | callbackStatements[callbackStatements.length - 1]; 72 | return ( 73 | callbackStatements 74 | .slice(0, callbackStatements.length - 1) 75 | .some(statement => { 76 | return ( 77 | statement.type === 'ReturnStatement' || 78 | (statement.type === 'IfStatement' && 79 | containsReturnRecursive(statement)) 80 | ); 81 | }) || 82 | (lastStatement && 83 | lastStatement.type === 'IfStatement' && 84 | containsReturnRecursive(lastStatement)) 85 | ); 86 | } 87 | 88 | const transformExpression = p => { 89 | const node = p.node; 90 | 91 | const blockStatement = node; 92 | 93 | // find the body statement and await in this block 94 | const {bodyStatement, awaitExpression} = containsAwaitOnPromise(p); 95 | const expressionIndex = blockStatement.body.indexOf(bodyStatement); 96 | 97 | const callExp = awaitExpression.argument; 98 | if (!callExp) { 99 | // eslint-disable-next-line no-console 100 | console.log('no argument', node.type, node.loc); 101 | return; 102 | } 103 | 104 | // insert a new await prior to this await expression using the callee object of the existing await expression 105 | let {errorCallBack, callBack, thenCalleeObject} = utils.parseCallExpression( 106 | callExp 107 | ); 108 | 109 | // Create await statement 110 | let firstAwaition; 111 | if (callBack.params && callBack.params.length > 0) { 112 | utils.resolveParamNameConflicts(j, p, callBack); 113 | const kind = utils.getParamsDeclarationKind(j, p, callBack); 114 | firstAwaition = utils.genAwaitionDeclarator( 115 | j, 116 | callExp, 117 | callBack, 118 | kind, 119 | callBack.params, 120 | thenCalleeObject 121 | ); 122 | } else { 123 | firstAwaition = j.expressionStatement( 124 | j.awaitExpression(thenCalleeObject) 125 | ); 126 | } 127 | 128 | let callbackStatements; 129 | if (callBack.body && callBack.body.type === 'BlockStatement') { 130 | callbackStatements = callBack.body.body; 131 | firstAwaition.comments = bodyStatement.comments; 132 | if (callbackStatements.length > 0) { 133 | bodyStatement.comments = 134 | callbackStatements[callbackStatements.length - 1].comments; 135 | } 136 | } else if (callBack.body) { 137 | callbackStatements = [j.returnStatement(callBack.body)]; 138 | } else { 139 | // eslint-disable-next-line no-console 140 | console.log('no callBack.body at', callBack.loc.start); 141 | return; 142 | } 143 | 144 | // detect a conditional return (any return prior to last), which is not currently supported 145 | if (bodyStatement.type === 'VariableDeclaration' && containsConditionalReturn(callbackStatements)) { 146 | // eslint-disable-next-line no-console 147 | console.log('conditional return not supported', node.type, node.loc.start); 148 | return; 149 | } 150 | 151 | // Transform return of callback 152 | const lastExp = callbackStatements[callbackStatements.length - 1]; 153 | // if lastExp is a return, use the argument 154 | const returnLast = lastExp && lastExp.type === 'ReturnStatement'; 155 | 156 | let bodyStatements = blockStatement.body; 157 | if (!bodyStatements) { 158 | // eslint-disable-next-line no-console 159 | console.log('no body', node.type, node.loc); 160 | return; 161 | } 162 | const prior = bodyStatements.slice(0, expressionIndex); 163 | const rest = bodyStatements.slice(expressionIndex + 1); 164 | const tryStatements = [ 165 | firstAwaition, 166 | ...(returnLast 167 | ? callbackStatements.slice(0, callbackStatements.length - 1) 168 | : callbackStatements) 169 | ]; 170 | if (returnLast || bodyStatement.type !== 'ReturnStatement') { 171 | const lastExpArgument = 172 | lastExp && (lastExp.expression || lastExp.argument || lastExp); 173 | if (!lastExpArgument) { 174 | // example: const p = await b().then(() => {}) 175 | // eslint-disable-next-line no-console 176 | console.log( 177 | 'no return expression', 178 | node.type, 179 | lastExp ? lastExp.loc.start : callBack.loc.start 180 | ); 181 | return; 182 | } 183 | 184 | // transform the existing await expression using the return of the then callback 185 | awaitExpression.argument = lastExpArgument; 186 | if (returnLast) { 187 | let expressionStatement; 188 | if (lastExpArgument.type === 'AssignmentExpression') { 189 | expressionStatement = j.expressionStatement(lastExpArgument); 190 | } else { 191 | expressionStatement = bodyStatement; 192 | } 193 | tryStatements.push(expressionStatement); 194 | } 195 | } 196 | blockStatement.body = errorCallBack 197 | ? [ 198 | ...prior, 199 | j.tryStatement( 200 | j.blockStatement(tryStatements), 201 | j.catchClause( 202 | errorCallBack.params[0], 203 | null, 204 | j.blockStatement(errorCallBack.body.body) 205 | ) 206 | ), 207 | ...rest 208 | ] 209 | : [...prior, ...tryStatements, ...rest]; 210 | 211 | return true; 212 | }; 213 | 214 | const replaceType = (type, filterer = containsAwaitOnPromise) => { 215 | // Loop until all promises are gone or no transforms are possible 216 | let somethingTransformed = false; 217 | let iterations = 0; 218 | const iterationsLimit = 256; 219 | do { 220 | iterations++; 221 | const paths = root.find(type).filter(filterer); 222 | if (paths.size() === 0) { 223 | break; 224 | } 225 | 226 | somethingTransformed = false; 227 | paths.forEach(path => { 228 | if (transformExpression(path)) { 229 | somethingTransformed = true; 230 | } 231 | }); 232 | } while (somethingTransformed && iterations < iterationsLimit); 233 | }; 234 | 235 | // replaceType(j.AwaitExpression); 236 | // replaceType(j.VariableDeclarator); 237 | replaceType(j.BlockStatement); 238 | 239 | // TODO: handle catch and finally blocks when unravelling 240 | // TODO: avoid changing behavior by unravelling a then callback with a conditional return 241 | // TODO: avoid changing behavior by unravelling a then with a parameter or local variable which masks a variable in an unravelled block 242 | 243 | return root.toSource(); 244 | }; 245 | -------------------------------------------------------------------------------- /lib/test-utils.js: -------------------------------------------------------------------------------- 1 | const defineInlineTest = require('jscodeshift/dist/testUtils').defineInlineTest; 2 | 3 | function extractBody(f) { 4 | if (typeof f === 'string') { 5 | return f; 6 | } 7 | 8 | const lines = f 9 | .toString() 10 | // split into lines 11 | .split('\n') 12 | // remove extra indent 13 | .map(line => line.substring(2)); 14 | 15 | // remove first and last lines (anonymous wrapper function) 16 | return lines.slice(1, lines.length - 1).join('\n'); 17 | } 18 | 19 | function defineTransformTestFromFunctions(transform, input, output, name) { 20 | defineInlineTest( 21 | transform, 22 | {}, 23 | extractBody(input), 24 | extractBody(output), 25 | name 26 | ); 27 | } 28 | 29 | module.exports = { 30 | defineTransformTestFromFunctions 31 | }; 32 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function recursiveGetObjectPatternNames(node, names) { 4 | if (node.type === 'Identifier' && node.name) { 5 | names.push(node.name); 6 | } else if (node.type === 'ObjectPattern') { 7 | // recurse through the object pattern 8 | for (const property of node.properties) { 9 | if (property && property.key && property.key.type === 'Identifier') { 10 | if (property.value && property.value.type === 'Identifier') { 11 | names.push(property.value.name); 12 | } else if (property.value && property.value.type === 'ObjectPattern') { 13 | recursiveGetObjectPatternNames(property.value, names); 14 | names.push(property.key.name); 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * Recursively walks up the path to find the names of all declarations in scope 23 | * @param {Path} p 24 | * @param {Array} [names] 25 | * @return {Array} 26 | */ 27 | function getNames(p, names = []) { 28 | if (p && p.value) { 29 | if (p.value.body && Array.isArray(p.value.body.body)) { 30 | for (const node of p.value.body.body) { 31 | if (node.declarations) { 32 | for (const declaration of node.declarations) { 33 | if (declaration.id) { 34 | if (declaration.id.type === 'ArrayPattern') { 35 | for (const element of declaration.id.elements) { 36 | element && names.push(element.name); 37 | } 38 | } else if (declaration.id.name) { 39 | names.push(declaration.id.name); 40 | } 41 | } 42 | } 43 | } 44 | if (node.id && node.id.name) { 45 | names.push(node.id.name); 46 | } 47 | } 48 | } 49 | 50 | if (p.value.params) { 51 | for (const param of p.value.params) { 52 | recursiveGetObjectPatternNames(param, names); 53 | } 54 | } 55 | } 56 | 57 | if (p.parentPath) { 58 | return getNames(p.parentPath, names); 59 | } else { 60 | return names; 61 | } 62 | } 63 | 64 | const suffixLimit = 9; 65 | function getUniqueName(namesInScope, param) { 66 | let safeName, 67 | name = param.name; 68 | if (!name) { 69 | return; 70 | } 71 | let i = 1; 72 | do { 73 | if (!namesInScope.includes(name)) { 74 | safeName = name; 75 | } else { 76 | i++; 77 | name = param.name + i; 78 | } 79 | } while (!safeName && i < suffixLimit); 80 | 81 | return safeName; 82 | } 83 | 84 | function renameElement(j, p, parent, element, namesInScope) { 85 | const newName = getUniqueName(namesInScope, element); 86 | if (!newName || newName === element.name) { 87 | // no safe name or name already unique 88 | return; 89 | } 90 | 91 | const rootScope = p.scope; 92 | const oldName = element.name; 93 | 94 | // rename usages of the element 95 | // this borrows heavily from the renameTo transform from VariableDeclarator in jscodeshift 96 | j(parent) 97 | .find(j.Identifier, {name: oldName}) 98 | .filter(function(path) { 99 | // ignore non-variables 100 | const parent = path.parent.node; 101 | 102 | if ( 103 | j.MemberExpression.check(parent) && 104 | parent.property === path.node && 105 | !parent.computed 106 | ) { 107 | // obj.oldName 108 | return false; 109 | } 110 | 111 | if ( 112 | j.Property.check(parent) && 113 | parent.key === path.node && 114 | !parent.computed 115 | ) { 116 | // { oldName: 3 } 117 | return false; 118 | } 119 | 120 | if ( 121 | j.MethodDefinition.check(parent) && 122 | parent.key === path.node && 123 | !parent.computed 124 | ) { 125 | // class A { oldName() {} } 126 | return false; 127 | } 128 | 129 | if ( 130 | j.JSXAttribute.check(parent) && 131 | parent.name === path.node && 132 | !parent.computed 133 | ) { 134 | // 135 | return false; 136 | } 137 | 138 | return true; 139 | }) 140 | .forEach(function(path) { 141 | let scope = path.scope; 142 | while (scope && scope !== rootScope) { 143 | if (scope.declares(oldName)) { 144 | return; 145 | } 146 | scope = scope.parent; 147 | } 148 | 149 | // identifier must refer to declared variable 150 | // It may look like we filtered out properties, 151 | // but the filter only ignored property "keys", not "value"s 152 | // In shorthand properties, "key" and "value" both have an 153 | // Identifier with the same structure. 154 | const parent = path.parent.node; 155 | if (j.Property.check(parent) && parent.shorthand && !parent.method) { 156 | path.parent.get('shorthand').replace(false); 157 | } 158 | 159 | path.get('name').replace(newName); 160 | }); 161 | 162 | // rename the element declaration 163 | element.name = newName; 164 | } 165 | 166 | const extractNamesFromIdentifierLike = id => { 167 | if (!id) { 168 | return []; 169 | } else if (id.type === 'ObjectPattern') { 170 | return id.properties 171 | .map( 172 | d => 173 | d.type === 'SpreadProperty' 174 | ? [d.argument.name] 175 | : extractNamesFromIdentifierLike(d.value) 176 | ) 177 | .reduce((acc, val) => acc.concat(val), []); 178 | } else if (id.type === 'ArrayPattern') { 179 | return id.elements 180 | .map(extractNamesFromIdentifierLike) 181 | .reduce((acc, val) => acc.concat(val), []); 182 | } else if (id.type === 'Identifier') { 183 | return [id.name]; 184 | } else if (id.type === 'RestElement') { 185 | return [id.argument.name]; 186 | } else { 187 | return []; 188 | } 189 | }; 190 | 191 | const isIdInElement = (element, name) => { 192 | return extractNamesFromIdentifierLike(element).indexOf(name) !== -1; 193 | }; 194 | 195 | function isParamMutated(j, p, parent, element) { 196 | // detect any reassignments of the element 197 | const reassigned = 198 | j(parent) 199 | .find(j.AssignmentExpression) 200 | .filter(function(path) { 201 | return extractNamesFromIdentifierLike(path.value.left).some(name => { 202 | return isIdInElement(element, name); 203 | }); 204 | }) 205 | .size() > 0; 206 | 207 | // detect any update, such as i++ of the element 208 | const hasUpdateMutation = 209 | j(parent) 210 | .find(j.UpdateExpression) 211 | .filter(n => { 212 | return isIdInElement(element, n.value.argument.name); 213 | }) 214 | .size() > 0; 215 | 216 | return reassigned || hasUpdateMutation; 217 | } 218 | 219 | module.exports = { 220 | isPromiseCall: node => { 221 | return ( 222 | node && 223 | node.type === 'CallExpression' && 224 | node.callee.property && 225 | (node.callee.property.name === 'then' || 226 | node.callee.property.name === 'spread' || 227 | (node.callee.property.name === 'catch' && 228 | node.callee.object && 229 | node.callee.object.type === 'CallExpression' && 230 | node.callee.object.callee.property && 231 | (node.callee.object.callee.property.name === 'then' || 232 | node.callee.object.callee.property.name === 'spread'))) 233 | ); 234 | }, 235 | 236 | genAwaitionDeclarator: (j, callExp, callBack, kind, params, exp) => { 237 | let declaratorId; 238 | if ( 239 | params.length > 1 || 240 | (callExp.callee && 241 | callExp.callee.property && 242 | callExp.callee.property.name === 'spread' && 243 | callBack.params) 244 | ) { 245 | declaratorId = j.arrayPattern(params); 246 | } else { 247 | declaratorId = params[0]; 248 | } 249 | 250 | return j.variableDeclaration(kind, [ 251 | j.variableDeclarator(declaratorId, j.awaitExpression(exp)) 252 | ]); 253 | }, 254 | 255 | /** 256 | * Determine the appropriate callbacks from the .catch or .then arguments of the call expression. 257 | * @param {Node} callExp 258 | * @return {{errorCallBack: Node, callBack: Node, thenCalleeObject: Node}} 259 | */ 260 | parseCallExpression: callExp => { 261 | let errorCallBack, callBack; 262 | let thenCalleeObject; 263 | if (callExp.callee.property && callExp.callee.property.name === 'catch') { 264 | errorCallBack = callExp.arguments[0]; 265 | callBack = callExp.callee.object.arguments[0]; 266 | thenCalleeObject = callExp.callee.object.callee.object; 267 | } else { 268 | callBack = callExp.arguments[0]; 269 | thenCalleeObject = callExp.callee.object; 270 | 271 | if (callExp.arguments[1]) { 272 | errorCallBack = callExp.arguments[1]; 273 | } 274 | } 275 | return {errorCallBack, callBack, thenCalleeObject}; 276 | }, 277 | 278 | /** 279 | * Resolves any name conflicts (renames the params) that would arise in path p from adding 280 | * variables based on the params of the callBack 281 | * @param j jscodeshift API facade 282 | * @param {Path} p The parent path 283 | * @param {Node} callBack 284 | */ 285 | resolveParamNameConflicts: (j, p, callBack) => { 286 | const namesInScope = getNames(p); 287 | for (const param of callBack.params) { 288 | if (param.type === 'ArrayPattern') { 289 | for (const element of param.elements) { 290 | if (element) { 291 | renameElement(j, p, callBack.body, element, namesInScope); 292 | } 293 | } 294 | } else { 295 | renameElement(j, p, callBack.body, param, namesInScope); 296 | } 297 | } 298 | }, 299 | 300 | /** 301 | * Renames any variable declarations or functions in body that would conflict with any names in path p 302 | * @param j jscodeshift API facade 303 | * @param {Path} p The parent path 304 | * @param {Body} body 305 | */ 306 | resolveNameConflicts: (j, p, body) => { 307 | const namesInScope = getNames(p); 308 | for (const node of body.body) { 309 | if (node.declarations) { 310 | for (const declaration of node.declarations) { 311 | if (declaration.id) { 312 | if (declaration.id.type === 'ArrayPattern') { 313 | for (const element of declaration.id.elements) { 314 | renameElement(j, p, body, element, namesInScope); 315 | } 316 | } else if (declaration.id.name) { 317 | renameElement(j, p, body, declaration.id, namesInScope); 318 | } 319 | } 320 | } 321 | } 322 | if (node.id && node.id.name) { 323 | renameElement(j, p, body, node.id, namesInScope); 324 | } 325 | } 326 | }, 327 | 328 | getParamsDeclarationKind: (j, p, callBack) => { 329 | let reassignment = false; 330 | for (const param of callBack.params) { 331 | if (isParamMutated(j, p, callBack.body, param)) { 332 | reassignment = true; 333 | } 334 | } 335 | 336 | // use const unless there is a mutation of one of the params 337 | return reassignment ? 'let' : 'const'; 338 | } 339 | }; 340 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-await-codemod", 3 | "version": "1.0.0", 4 | "description": "async-await codemod scripts for JSCodeshift", 5 | "repository": { 6 | "url": "git@github.com:sgilroy/async-await-codemod.git", 7 | "type": "git" 8 | }, 9 | "author": "Scott Gilroy ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "eslint": "^4.19.1", 13 | "eslint-config-prettier": "^2.9.0", 14 | "eslint-plugin-prettier": "^2.6.0", 15 | "jest": "^25.0.0", 16 | "prettier": "^1.13.4" 17 | }, 18 | "scripts": { 19 | "test": "npm run lint && jest", 20 | "fix": "npm run eslint -- --fix & npm run prettier -- --write & wait", 21 | "lint": "npm run eslint & wait", 22 | "prettier": "prettier -l README.md package.json", 23 | "eslint": "eslint ." 24 | }, 25 | "prettier": { 26 | "singleQuote": true, 27 | "bracketSpacing": false 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "eslint:recommended", 32 | "plugin:prettier/recommended" 33 | ], 34 | "env": { 35 | "node": true, 36 | "es6": true, 37 | "mocha": true 38 | }, 39 | "parserOptions": { 40 | "ecmaVersion": 2017 41 | } 42 | }, 43 | "dependencies": { 44 | "jscodeshift": "^0.6.4" 45 | } 46 | } 47 | --------------------------------------------------------------------------------