Skip to content

CG-10697: TS-Promise-Chain #417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions codegen-examples/examples/promises_to_async_await/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Transform promise.then statements to async/await

This example demonstrates how to use Codegen to automatically transform `promise.then` statements to `async/await`.

> Here is an [open pull request](https://github.com/twilio/twilio-node/pull/1072) created in the [_official_ twilio node.js](https://www.twilio.com/docs/messaging/quickstart/node) repository using the promise to async/await transformation using Codegen.

## How the Migration Script Works

The script automates the entire migration process in a few key steps:

1. **Promise Detection**

```python
# Get all promise chains in the codebase
promise_chains = []
for file in codebase.files:
promise_chains = promise_chains + file.promise_chains
```

```python
# Or get all promise chains in the current function
function = codebase.get_function("function_name")
promise_chains = function.promise_chains
```

```python
# Or get the promise chain for the current function call
function_call = codebase.get_function("function_name").function_calls[0]
promise_chain = function_call.promise_chain
```

- Automatically identifies all promise chains in each file, function, or function call in the codebase
- Uses Codegen's intelligent code analysis engine

1. **Transformation**

```python
# Transform a promise chain to async/await (inplace)
promise_chain.convert_to_async_await()
codebase.commit()
```

```python
# Or return the transformed code + clean up additonal business logic from .then blocks
promise_statement = promise_chain.parent_statement
new_code = promise_chain.convert_to_async_await(inplace_edit=False)

promise_statement.edit(
f"""
{new_code}

# handle additional business logic here
"""
)
```

- Replaces the promise chain with the async/await version
- Handles function calls and function bodies automatically
- Handles top-level varable assignments and return statements
- Deals with ambiguous return blocks by adding annonymous functions where necessary
- Carries over try/catch/finally blocks
- Acknowledges implicit returns

# Examples

## Running the Migration on the [Official Twilio Node.js](https://github.com/twilio/twilio-node) Client Libary

_1. Follow step by step in the [convert_promises_twilio_repository.ipynb](./convert_promises_twilio_repository.ipynb) notebook_

_Or run codemod script directly:_

```bash
# Install Codegen
pip install codegen

# Run the promise to async/await migration
python run.py
```

The script will:

1. Initialize the codebase
1. Find _all 592_ instances of `promise.then` statements with the base call called `operationPromise`
1. Convert those to async/await style calls

_IMPORTANT: ensure to run `npx prettier --write .` after the migration to fix indentation + linting_

## Explore All The Covered Cases for the Conversion

_Checkout the [promise_to_async_await.ipynb](./promise_to_async_await.ipynb) notebook_

Currently, the `promise_chain.convert_to_async_await()` method handles the following cases:

- `promise.then()` statements of any length
- `promise.then().catch()` statements of any length
- `promise.then().catch().finally()` statements of any length
- Implicit returns -> `return promise.then()`
- Top level variable assignments -> `let assigned_var = promise.then()`
- Top level variable assignments -> `let assigned_var = promise.then()`
- Ambiguous/conditional return blocks

**IMPORTANT:**

_There will be cases that the current `promise_chain.convert_to_async_await()` cannot handle. In those cases, either right your own transformation logic using the codegen-sdk or open an issue on the [Codegen](https://github.com/codegen-sh/codegen-sdk) repository._

## Contributing

Feel free to submit issues and any enhancement requests!
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Convert Promises to Async/Await in the Twilio Node.js Client Library using Codegen\n",
"\n",
"This notebook will show you how to convert a repeated use of a promise chain to async await \n",
"in the official twilio js client library twilio/twilio-node repository.\n",
"\n",
"1. Finds all methods containing operationPromise.then chains\n",
"2. Converts the promise chain to use async await\n",
"3. Gets rid of the callback handler by adding try catch directly in the function body"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from codegen import Codebase\n",
"from codegen.sdk.enums import ProgrammingLanguage\n",
"from codegen.sdk.core.statements.statement import StatementType"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"Initializing codebase...\")\n",
"codebase = Codebase(\"twilio/twilio-node\", programming_language=ProgrammingLanguage.TYPESCRIPT)\n",
"print(\"Twilio repository initialized!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 1. Find all Promise Chains in class methods with the base call being `operationPromise.then`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# loop through all files -> classes -> methods to find promise the operationPromise chains\n",
"operation_promise_chains = []\n",
"unique_files = set()\n",
"\n",
"i = 0\n",
"\n",
"for _class in codebase.classes:\n",
" for method in _class.methods:\n",
" if method.name in [\"each\", \"setPromiseCallback\"]:\n",
" print(\"skipping method\", method.name, \"...\")\n",
" continue\n",
"\n",
" # Only process methods containing operationPromise\n",
" if not method.find(\"operationPromise\"):\n",
" continue\n",
"\n",
" # Find the first promise chain with then blocks\n",
" for promise_chain in method.promise_chains:\n",
" promise_statement = promise_chain.parent_statement\n",
" operation_promise_chains.append({\"function_name\": method.name, \"promise_chain\": promise_chain, \"promise_statement\": promise_statement})\n",
" unique_files.add(method.file.filepath)\n",
" i += 1\n",
" if i < 10:\n",
" print(f\"Found operation promise in the {method.name} method in {method.file.filepath} file.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"Number of Operation Promise Chains found:\", len(operation_promise_chains))\n",
"print(\"Number of files affected:\", len(unique_files))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2. Convert *all* 592 Promise Chains to Async/Await \n",
"\n",
"*Additionally...* Add a Try/Catch to Eliminate the Callback Handler"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"i = 0\n",
"assignment_variable_name = \"operation\"\n",
"\n",
"for promise_chain_dict in operation_promise_chains:\n",
" promise_chain = promise_chain_dict[\"promise_chain\"]\n",
" promise_statement = promise_chain_dict[\"promise_statement\"]\n",
" function_name = promise_chain_dict[\"function_name\"]\n",
" # ---------- CONVERT PROMISE CHAIN TO ASYNC AWAIT ----------\n",
" async_await_code = promise_chain.convert_to_async_await(assignment_variable_name=assignment_variable_name, inplace_edit=False)\n",
"\n",
" if i < 10:\n",
" print(f\"converting {function_name} promise chain to async/await.\")\n",
"\n",
" i += 1\n",
" # ---------- ADD TRY CATCH BLOCK INSTEAD OF CALLBACK HANDLER ----------\n",
" new_code = f\"\"\"\\\n",
" try {{\n",
" {async_await_code}\n",
"\n",
" if (callback) {{\n",
" callback(null, {assignment_variable_name});\n",
" }}\n",
"\n",
" return {assignment_variable_name};\n",
" }} catch(err: any) {{\n",
" if (callback) {{\n",
" callback(err);\n",
" }}\n",
" throw err;\n",
" }}\"\"\"\n",
"\n",
" promise_statement.edit(new_code)\n",
"\n",
" # ---------- CLEAN UP CALLBACK HANDLER ASSIGNMENT AND SUBSEQUENT RETURN STATEMENT ----------\n",
" statements = promise_statement.parent.get_statements()\n",
" return_stmt = next((stmt for stmt in statements if stmt.statement_type == StatementType.RETURN_STATEMENT), None)\n",
" assign_stmt = next((stmt for stmt in reversed(statements) if stmt.statement_type == StatementType.ASSIGNMENT), None)\n",
"\n",
" if return_stmt:\n",
" return_stmt.remove()\n",
" if assign_stmt:\n",
" assign_stmt.remove()\n",
"\n",
"codebase.commit()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"After making the changes, ensure to run *`npx prettier --write .`* to format line indentation and linting errors."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# reset the formatting\n",
"codebase.reset()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"codebase = Codebase(\"/Users/tawsifkamal/Documents/codegen-repos/twilio-node\", programming_language=ProgrammingLanguage.TYPESCRIPT)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Loading