Skip to content

Commit 6707a50

Browse files
authored
CG-10697: TS-Promise-Chain (#417)
- Adds TSPromise class to help deal with promises more easily - adds ability to retreive TSPromiseChain from a functionCall node - convert to async/await blog - convert to async/await tutorial - convert to async/await example folder
1 parent 0f437b4 commit 6707a50

File tree

19 files changed

+2897
-3
lines changed

19 files changed

+2897
-3
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Transform promise.then statements to async/await
2+
3+
This example demonstrates how to use Codegen to automatically transform `promise.then` statements to `async/await`.
4+
5+
> 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.
6+
7+
## How the Migration Script Works
8+
9+
The script automates the entire migration process in a few key steps:
10+
11+
1. **Promise Detection**
12+
13+
```python
14+
# Get all promise chains in the codebase
15+
promise_chains = []
16+
for file in codebase.files:
17+
promise_chains = promise_chains + file.promise_chains
18+
```
19+
20+
```python
21+
# Or get all promise chains in the current function
22+
function = codebase.get_function("function_name")
23+
promise_chains = function.promise_chains
24+
```
25+
26+
```python
27+
# Or get the promise chain for the current function call
28+
function_call = codebase.get_function("function_name").function_calls[0]
29+
promise_chain = function_call.promise_chain
30+
```
31+
32+
- Automatically identifies all promise chains in each file, function, or function call in the codebase
33+
- Uses Codegen's intelligent code analysis engine
34+
35+
1. **Transformation**
36+
37+
```python
38+
# Transform a promise chain to async/await (inplace)
39+
promise_chain.convert_to_async_await()
40+
codebase.commit()
41+
```
42+
43+
```python
44+
# Or return the transformed code + clean up additonal business logic from .then blocks
45+
promise_statement = promise_chain.parent_statement
46+
new_code = promise_chain.convert_to_async_await(inplace_edit=False)
47+
48+
promise_statement.edit(
49+
f"""
50+
{new_code}
51+
52+
# handle additional business logic here
53+
"""
54+
)
55+
```
56+
57+
- Replaces the promise chain with the async/await version
58+
- Handles function calls and function bodies automatically
59+
- Handles top-level varable assignments and return statements
60+
- Deals with ambiguous return blocks by adding annonymous functions where necessary
61+
- Carries over try/catch/finally blocks
62+
- Acknowledges implicit returns
63+
64+
# Examples
65+
66+
## Running the Migration on the [Official Twilio Node.js](https://github.com/twilio/twilio-node) Client Libary
67+
68+
_1. Follow step by step in the [convert_promises_twilio_repository.ipynb](./convert_promises_twilio_repository.ipynb) notebook_
69+
70+
_Or run codemod script directly:_
71+
72+
```bash
73+
# Install Codegen
74+
pip install codegen
75+
76+
# Run the promise to async/await migration
77+
python run.py
78+
```
79+
80+
The script will:
81+
82+
1. Initialize the codebase
83+
1. Find _all 592_ instances of `promise.then` statements with the base call called `operationPromise`
84+
1. Convert those to async/await style calls
85+
86+
_IMPORTANT: ensure to run `npx prettier --write .` after the migration to fix indentation + linting_
87+
88+
## Explore All The Covered Cases for the Conversion
89+
90+
_Checkout the [promise_to_async_await.ipynb](./promise_to_async_await.ipynb) notebook_
91+
92+
Currently, the `promise_chain.convert_to_async_await()` method handles the following cases:
93+
94+
- `promise.then()` statements of any length
95+
- `promise.then().catch()` statements of any length
96+
- `promise.then().catch().finally()` statements of any length
97+
- Implicit returns -> `return promise.then()`
98+
- Top level variable assignments -> `let assigned_var = promise.then()`
99+
- Top level variable assignments -> `let assigned_var = promise.then()`
100+
- Ambiguous/conditional return blocks
101+
102+
**IMPORTANT:**
103+
104+
_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._
105+
106+
## Contributing
107+
108+
Feel free to submit issues and any enhancement requests!
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Convert Promises to Async/Await in the Twilio Node.js Client Library using Codegen\n",
8+
"\n",
9+
"This notebook will show you how to convert a repeated use of a promise chain to async await \n",
10+
"in the official twilio js client library twilio/twilio-node repository.\n",
11+
"\n",
12+
"1. Finds all methods containing operationPromise.then chains\n",
13+
"2. Converts the promise chain to use async await\n",
14+
"3. Gets rid of the callback handler by adding try catch directly in the function body"
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"from codegen import Codebase\n",
24+
"from codegen.sdk.enums import ProgrammingLanguage\n",
25+
"from codegen.sdk.core.statements.statement import StatementType"
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {},
32+
"outputs": [],
33+
"source": [
34+
"print(\"Initializing codebase...\")\n",
35+
"codebase = Codebase(\"twilio/twilio-node\", programming_language=ProgrammingLanguage.TYPESCRIPT)\n",
36+
"print(\"Twilio repository initialized!\")"
37+
]
38+
},
39+
{
40+
"cell_type": "markdown",
41+
"metadata": {},
42+
"source": [
43+
"### 1. Find all Promise Chains in class methods with the base call being `operationPromise.then`"
44+
]
45+
},
46+
{
47+
"cell_type": "code",
48+
"execution_count": null,
49+
"metadata": {},
50+
"outputs": [],
51+
"source": [
52+
"# loop through all files -> classes -> methods to find promise the operationPromise chains\n",
53+
"operation_promise_chains = []\n",
54+
"unique_files = set()\n",
55+
"\n",
56+
"i = 0\n",
57+
"\n",
58+
"for _class in codebase.classes:\n",
59+
" for method in _class.methods:\n",
60+
" if method.name in [\"each\", \"setPromiseCallback\"]:\n",
61+
" print(\"skipping method\", method.name, \"...\")\n",
62+
" continue\n",
63+
"\n",
64+
" # Only process methods containing operationPromise\n",
65+
" if not method.find(\"operationPromise\"):\n",
66+
" continue\n",
67+
"\n",
68+
" # Find the first promise chain with then blocks\n",
69+
" for promise_chain in method.promise_chains:\n",
70+
" promise_statement = promise_chain.parent_statement\n",
71+
" operation_promise_chains.append({\"function_name\": method.name, \"promise_chain\": promise_chain, \"promise_statement\": promise_statement})\n",
72+
" unique_files.add(method.file.filepath)\n",
73+
" i += 1\n",
74+
" if i < 10:\n",
75+
" print(f\"Found operation promise in the {method.name} method in {method.file.filepath} file.\")"
76+
]
77+
},
78+
{
79+
"cell_type": "code",
80+
"execution_count": null,
81+
"metadata": {},
82+
"outputs": [],
83+
"source": [
84+
"print(\"Number of Operation Promise Chains found:\", len(operation_promise_chains))\n",
85+
"print(\"Number of files affected:\", len(unique_files))"
86+
]
87+
},
88+
{
89+
"cell_type": "markdown",
90+
"metadata": {},
91+
"source": [
92+
"### 2. Convert *all* 592 Promise Chains to Async/Await \n",
93+
"\n",
94+
"*Additionally...* Add a Try/Catch to Eliminate the Callback Handler"
95+
]
96+
},
97+
{
98+
"cell_type": "code",
99+
"execution_count": null,
100+
"metadata": {},
101+
"outputs": [],
102+
"source": [
103+
"i = 0\n",
104+
"assignment_variable_name = \"operation\"\n",
105+
"\n",
106+
"for promise_chain_dict in operation_promise_chains:\n",
107+
" promise_chain = promise_chain_dict[\"promise_chain\"]\n",
108+
" promise_statement = promise_chain_dict[\"promise_statement\"]\n",
109+
" function_name = promise_chain_dict[\"function_name\"]\n",
110+
" # ---------- CONVERT PROMISE CHAIN TO ASYNC AWAIT ----------\n",
111+
" async_await_code = promise_chain.convert_to_async_await(assignment_variable_name=assignment_variable_name, inplace_edit=False)\n",
112+
"\n",
113+
" if i < 10:\n",
114+
" print(f\"converting {function_name} promise chain to async/await.\")\n",
115+
"\n",
116+
" i += 1\n",
117+
" # ---------- ADD TRY CATCH BLOCK INSTEAD OF CALLBACK HANDLER ----------\n",
118+
" new_code = f\"\"\"\\\n",
119+
" try {{\n",
120+
" {async_await_code}\n",
121+
"\n",
122+
" if (callback) {{\n",
123+
" callback(null, {assignment_variable_name});\n",
124+
" }}\n",
125+
"\n",
126+
" return {assignment_variable_name};\n",
127+
" }} catch(err: any) {{\n",
128+
" if (callback) {{\n",
129+
" callback(err);\n",
130+
" }}\n",
131+
" throw err;\n",
132+
" }}\"\"\"\n",
133+
"\n",
134+
" promise_statement.edit(new_code)\n",
135+
"\n",
136+
" # ---------- CLEAN UP CALLBACK HANDLER ASSIGNMENT AND SUBSEQUENT RETURN STATEMENT ----------\n",
137+
" statements = promise_statement.parent.get_statements()\n",
138+
" return_stmt = next((stmt for stmt in statements if stmt.statement_type == StatementType.RETURN_STATEMENT), None)\n",
139+
" assign_stmt = next((stmt for stmt in reversed(statements) if stmt.statement_type == StatementType.ASSIGNMENT), None)\n",
140+
"\n",
141+
" if return_stmt:\n",
142+
" return_stmt.remove()\n",
143+
" if assign_stmt:\n",
144+
" assign_stmt.remove()\n",
145+
"\n",
146+
"codebase.commit()"
147+
]
148+
},
149+
{
150+
"cell_type": "markdown",
151+
"metadata": {},
152+
"source": [
153+
"After making the changes, ensure to run *`npx prettier --write .`* to format line indentation and linting errors."
154+
]
155+
},
156+
{
157+
"cell_type": "code",
158+
"execution_count": null,
159+
"metadata": {},
160+
"outputs": [],
161+
"source": [
162+
"# reset the formatting\n",
163+
"codebase.reset()"
164+
]
165+
},
166+
{
167+
"cell_type": "code",
168+
"execution_count": null,
169+
"metadata": {},
170+
"outputs": [],
171+
"source": [
172+
"codebase = Codebase(\"/Users/tawsifkamal/Documents/codegen-repos/twilio-node\", programming_language=ProgrammingLanguage.TYPESCRIPT)"
173+
]
174+
}
175+
],
176+
"metadata": {
177+
"kernelspec": {
178+
"display_name": ".venv",
179+
"language": "python",
180+
"name": "python3"
181+
},
182+
"language_info": {
183+
"codemirror_mode": {
184+
"name": "ipython",
185+
"version": 3
186+
},
187+
"file_extension": ".py",
188+
"mimetype": "text/x-python",
189+
"name": "python",
190+
"nbconvert_exporter": "python",
191+
"pygments_lexer": "ipython3",
192+
"version": "3.13.0"
193+
}
194+
},
195+
"nbformat": 4,
196+
"nbformat_minor": 2
197+
}

0 commit comments

Comments
 (0)