GithubHelp home page GithubHelp logo

codemodsquad / asyncify Goto Github PK

View Code? Open in Web Editor NEW
13.0 3.0 6.0 1.08 MB

Don't keep your promises ๐Ÿ˜‰

License: MIT License

JavaScript 2.74% TypeScript 97.26%
async await promise chain then thenable codemod refactoring

asyncify's Introduction

@codemodsquad/asyncify

CircleCI Coverage Status semantic-release Commitizen friendly npm version

Transforms promise chains into async/await. I wrote this to refactor the 5000+ .then/.catch/.finally calls in the sequelize codebase. This is slightly inspired by async-await-codemod, but written from scratch to guarantee that it doesn't change the behavior of the transformed code, and keeps the code reasonably tidy.

Usage

npx @codemodsquad/asyncify path/to/your/project/**/*.js

This command just forwards to jscodeshift, you can pass other jscodeshift CLI options.

Support table

asyncify
Converts .then โœ…
Converts .catch โœ…
Converts .finally โœ…
Renames identifiers in handlers that would conflict โœ…
Converts promise chains that aren't returned/awaited into IIAAFs โœ…
Converts return Promise.resolve()/return Promise.reject() โœ…
Removes unnecessary Promise.resolve() wrappers โœ…
Warns when the original function could return/throw a non-promise Planned
Refactoring/inlining handlers that contain conditional returns
All but one if/else/switch branch return โœ…
All branches return, even nested ones โœ…
All but one nested if/else/switch branch return ๐Ÿšซ
More than one if/else/switch branch doesn't return ๐Ÿšซ
Return inside loop ๐Ÿšซ

Warnings

Comments can sometimes get deleted due to an impedance mismatch between @babel and recast ASTs. If you use the --commentWorkarounds=true option it will try to prevent more comments from getting deleted but it sometimes causes an assertion to fail in recast.

There are a few edge cases where asyncify produces funky output. It's intended to not break any existing behavior (I know of no cases where it does, and I have fixed several such issues) but sometimes the output will be be semantically wrong even if it behaves correctly. For example, I've seen a case where doing an async operation several times in a row:

it('test', () => {
  const doSomething = () => {
    // ...
  }

  return doSomething()
    .then(doSomething)
    .then(doSomething)
})

Gets converted to:

it('test', async () => {
  const doSomething = () => {
    // ...
  }
  await doSomething(await doSomething(await doSomething()))
})

This works even though it initially seems like it wouldn't and is obviously not what you want:

it('test', async () => {
  const doSomething = () => {
    // ...
  }
  await doSomething()
  await doSomething()
  await doSomething()
})

Although I could possibly fix this for cases where it's easy to determine that the function has no parameters, there could be cases where it's impossible to determine whether the identifier doSomething is even a function or whether it has parameters.

Disabling recast workaround

At the time I wrote asyncify, there were some show-stopping bugs in old version of recast that jscodeshift depended on. To avoid this problem, asyncify parses with a newer version of recast in its own dependencies, instead of parsing with the jscodeshift API. The author of putout has asked to be able to parse with the injected jscodeshift API for performance, so you can access that version of the jscodeshift transform as:

import transform from '@codemodsquad/asyncify/noRecastWorkaround'

Or there are two ways you can do it when running via jscodeshift:

jscodeshift -t path/to/asyncify/noRecastWorkaround.js
jscodeshift -t path/to/asyncify/index.js --noRecastWorkaround=true

asyncify's People

Contributors

coderaiser avatar jedwards1211 avatar yonran avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

asyncify's Issues

behavioral change for some unawaited chains

Failing test case:

export const input = `
it('should support ordering with only belongsTo includes', function() {
  const User = this.sequelize.define('User', {}),
    Item = this.sequelize.define('Item', { 'test': DataTypes.STRING }),
    Order = this.sequelize.define('Order', { 'position': DataTypes.INTEGER });

  User.belongsTo(Item, { 'as': 'itemA', foreignKey: 'itemA_id' });
  User.belongsTo(Item, { 'as': 'itemB', foreignKey: 'itemB_id' });
  User.belongsTo(Order);

  return this.sequelize.sync().then(() => {
    return promiseProps({
      users: User.bulkCreate([{}, {}, {}]).then(() => {
        return User.findAll();
      }),
      items: Item.bulkCreate([
        { 'test': 'abc' },
        { 'test': 'def' },
        { 'test': 'ghi' },
        { 'test': 'jkl' }
      ]).then(() => {
        return Item.findAll({ order: ['id'] });
      }),
      orders: Order.bulkCreate([
        { 'position': 2 },
        { 'position': 3 },
        { 'position': 1 }
      ]).then(() => {
        return Order.findAll({ order: ['id'] });
      })
    }).then(results => {
      const user1 = results.users[0];
      const user2 = results.users[1];
      const user3 = results.users[2];

      const item1 = results.items[0];
      const item2 = results.items[1];
      const item3 = results.items[2];
      const item4 = results.items[3];

      const order1 = results.orders[0];
      const order2 = results.orders[1];
      const order3 = results.orders[2];

      return Promise.all([
        user1.setItemA(item1),
        user1.setItemB(item2),
        user1.setOrder(order3),
        user2.setItemA(item3),
        user2.setItemB(item4),
        user2.setOrder(order2),
        user3.setItemA(item1),
        user3.setItemB(item4),
        user3.setOrder(order1)
      ]);
    }).then(() => {
      return User.findAll({
        'include': [
          { 'model': Item, 'as': 'itemA', where: { test: 'abc' } },
          { 'model': Item, 'as': 'itemB' },
          Order],
        'order': [
          [Order, 'position']
        ]
      }).then(as => {
        expect(as.length).to.eql(2);

        expect(as[0].itemA.test).to.eql('abc');
        expect(as[1].itemA.test).to.eql('abc');

        expect(as[0].Order.position).to.eql(1);
        expect(as[1].Order.position).to.eql(2);
      });
    });
  });
});
`

export const options = {}

export const expected = `
it('should support ordering with only belongsTo includes', async function() {
  const User = this.sequelize.define('User', {}),
    Item = this.sequelize.define('Item', { 'test': DataTypes.STRING }),
    Order = this.sequelize.define('Order', { 'position': DataTypes.INTEGER });

  User.belongsTo(Item, { 'as': 'itemA', foreignKey: 'itemA_id' });
  User.belongsTo(Item, { 'as': 'itemB', foreignKey: 'itemB_id' });
  User.belongsTo(Order);

  await this.sequelize.sync();

  const results = await promiseProps({
    users: User.bulkCreate([{}, {}, {}]).then(() => {
      return User.findAll()
    }),
    items: Item.bulkCreate([
      { 'test': 'abc' },
      { 'test': 'def' },
      { 'test': 'ghi' },
      { 'test': 'jkl' }
    ]).then(() => {
      return Item.findAll({ order: ['id'] })
    }),
    orders: Order.bulkCreate([
      { 'position': 2 },
      { 'position': 3 },
      { 'position': 1 }
    ]).then(() => {
      return Order.findAll({ order: ['id'] });
    })
  });

  const user1 = results.users[0];
  const user2 = results.users[1];
  const user3 = results.users[2];

  const item1 = results.items[0];
  const item2 = results.items[1];
  const item3 = results.items[2];
  const item4 = results.items[3];

  const order1 = results.orders[0];
  const order2 = results.orders[1];
  const order3 = results.orders[2];

  await Promise.all([
    user1.setItemA(item1),
    user1.setItemB(item2),
    user1.setOrder(order3),
    user2.setItemA(item3),
    user2.setItemB(item4),
    user2.setOrder(order2),
    user3.setItemA(item1),
    user3.setItemB(item4),
    user3.setOrder(order1)
  ]);

  const as = await User.findAll({
    'include': [
      { 'model': Item, 'as': 'itemA', where: { test: 'abc' } },
      { 'model': Item, 'as': 'itemB' },
      Order],
    'order': [
      [Order, 'position']
    ]
  });

  expect(as.length).to.eql(2);

  expect(as[0].itemA.test).to.eql('abc');
  expect(as[1].itemA.test).to.eql('abc');

  expect(as[0].Order.position).to.eql(1);
  expect(as[1].Order.position).to.eql(2);
});
`

No index.js, fails to compile

Following the straight-forward instructions in the README:

git clone https://github.com/codemodsquad/asyncify
npx jscodeshift -t asyncify/index.js path/to/your/project/**/*.js

It seems there is a step missing. There is no index.js in the root of the project, and running yarn build fails with

> babel src --out-dir es --extensions ".ts" --source-maps inline && cross-env BABEL_ENV=es5 babel src --out-dir . --extensions ".ts"

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /<snip>/asyncify/node_modules/@babel/preset-env/node_modules/@babel/helper-compilation-targets/package.json

Is there a missing step? I looked at the circleci job and there don't seem to be any special env vars or other configurations in place, but there also aren't any artifacts published.

better option to not convert short chains to IIAAFs

Cases like

const promises = Promise.all([
  user.update().then(() => user.reload()),
  post.update().then(() => post.reload()),
])

Would be better left alone since they become messy when converted to IIAAFs

Unexpected comment placement after transformation

Again, thanks for this code mod. It works very well so far!

I just wanted to pass on some examples where comments in the transformed code were not placed like I expected them to be.

Environment

  • @codemodsquad/asyncify: 2.0.5
  • jscodeshift: 0.11.0
    • babel: 7.10.2
    • babylon: 7.10.2
    • flow: 0.138.0
    • recast: 0.20.4

Input

function a (x) {
    // Apply f to x because ...
    return f(x)
        .then(() => x);
}

function b (x) {
    return f(x)
        // Always return true because ...
        .then(() => true);
}

Actual Output

async function a(x) {
    await f(x);
    // Apply f to x because ...
    return x;
}

async function b(x) {
    await // Always return true because ...
    f(x);

    return true;
}

Expected Output

async function a(x) {
    // Apply f to x because ...
    await f(x);
    return x;
}

async function b(x) {
    await f(x);
    // Always return true because ...
    return true;
}

Doesn't preserve semantics for non-function passed to onFulfilled/onRejected

I forgot about this important detail of .then:

If it is not a function, it is internally replaced with an "Identity" function (it returns the received argument).

Right now asyncify always replaces A.then(B) with await B(await A) if B isn't a function expression. I need to walk the scope to determine if it's a function expression or not, and if it's impossible to tell, replace with a runtime check and developer warning.

incorrect output for multiple else if branches that fall through

Input

async function createUser(args) {
  const {username, groups} = args
  const user = await Users.create({username})
    .then(user => {
      if (groups) {
        return addUserToGroups(user, groups)
      } else if (foo) {
        console.log('test')
      } else if (bar) {
        console.log('test2')
      }
      console.log('blah')
      return user
    })
    .then(user => {
      if (groups) {
        console.log('test')
      } else {
        return 'noGroups'
      }
      return 'user'
    })
    .catch(err => {
      console.error(err.stack)
      return dummyUser()
    })
}

Expected

async function createUser(args) {
  const {username, groups} = args
  let user
  try {
    const user1 = await Users.create({ username })
    const user0 = await (async user => {
      if (groups) {
        return addUserToGroups(user, groups)
      } else if (foo) {
        console.log('test')
      } else if (bar) {
        console.log('test2')
      }
      console.log('blah')
      return user
    })(user1)
    if (groups) {
      console.log('test')
      user = 'user'
    } else {
      user = 'noGroups'
    }
  } catch (err) {
    console.error(err.stack)
    user = await dummyUser()
  }
}

Actual

async function createUser(args) {
  const {username, groups} = args
  let user
  try {
    let user0
    const user1 = await Users.create({ username })
    if (groups) {
      user0 = await addUserToGroups(user1, groups)
    } else if (foo) {
      console.log('test')
    } else if (bar) {
      console.log('test2')
    } else {
      console.log('blah')
      user0 = await user1
    }
    if (groups) {
      console.log('test')
      user = 'user'
    } else {
      user = 'noGroups'
    }
  } catch (err) {
    console.error(err.stack)
    user = await dummyUser()
  }
}
`

Process hangs with 100% CPU utilization

First of all thanks for the great work! This code mod seems to do a fantastic job from what I saw up until now.

Unfortunately there was a problem with one particular source file. To reproduce:

git clone https://github.com/apache/cordova-android.git
cd cordova-android
npm i jscodeshift @codemodsquad/asyncify
npx jscodeshift --verbose=2 -t node_modules/@codemodsquad/asyncify/index.js bin/templates/cordova/lib/check_reqs.js

There is no output from the process and it stays at 100% CPU utilization indefinitely (well, after 20 min or so I killed it).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.