esupgrade 2025.0.0 → 2025.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pre-commit-config.yaml +7 -6
- package/README.md +57 -48
- package/bin/esupgrade.js +7 -21
- package/images/logo-dark.svg +3 -4
- package/images/logo-light.svg +1 -2
- package/package.json +1 -1
- package/src/index.js +18 -689
- package/src/newlyAvailable.js +154 -0
- package/src/widelyAvailable.js +265 -0
- package/tests/cli.test.js +358 -0
- package/tests/index.test.js +274 -0
- package/tests/newlyAvailable.test.js +221 -0
- package/tests/widelyAvailable.test.js +428 -0
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/esupgrade.iml +0 -8
- package/.idea/inspectionProfiles/Project_Default.xml +0 -28
- package/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- package/.idea/misc.xml +0 -7
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -7
- package/tests/transform.test.js +0 -322
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform new Promise((resolve, reject) => { resolve(fn()) }) to Promise.try(fn)
|
|
3
|
+
*/
|
|
4
|
+
export function promiseTry(j, root) {
|
|
5
|
+
let modified = false
|
|
6
|
+
const changes = []
|
|
7
|
+
|
|
8
|
+
root
|
|
9
|
+
.find(j.NewExpression)
|
|
10
|
+
.filter((path) => {
|
|
11
|
+
const node = path.node
|
|
12
|
+
// Check if this is new Promise(...)
|
|
13
|
+
if (!j.Identifier.check(node.callee) || node.callee.name !== "Promise") {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Skip if this Promise is being awaited
|
|
18
|
+
// Check if parent is an AwaitExpression
|
|
19
|
+
if (path.parent && j.AwaitExpression.check(path.parent.node)) {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if there's one argument that's a function
|
|
24
|
+
if (node.arguments.length !== 1) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const executor = node.arguments[0]
|
|
29
|
+
if (
|
|
30
|
+
!j.ArrowFunctionExpression.check(executor) &&
|
|
31
|
+
!j.FunctionExpression.check(executor)
|
|
32
|
+
) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if function has 1-2 params (resolve, reject)
|
|
37
|
+
if (executor.params.length < 1 || executor.params.length > 2) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if body is a block with single resolve() call or expression body
|
|
42
|
+
const body = executor.body
|
|
43
|
+
|
|
44
|
+
// For arrow functions with expression body: (resolve) => expr
|
|
45
|
+
if (!j.BlockStatement.check(body)) {
|
|
46
|
+
// Check if expression is resolve(something) or func(resolve)
|
|
47
|
+
if (j.CallExpression.check(body)) {
|
|
48
|
+
const callExpr = body
|
|
49
|
+
// Pattern: (resolve) => resolve(expr)
|
|
50
|
+
if (
|
|
51
|
+
j.Identifier.check(callExpr.callee) &&
|
|
52
|
+
j.Identifier.check(executor.params[0]) &&
|
|
53
|
+
callExpr.callee.name === executor.params[0].name &&
|
|
54
|
+
callExpr.arguments.length > 0
|
|
55
|
+
) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
// Pattern: (resolve) => func(resolve) - resolve must be the ONLY argument
|
|
59
|
+
if (
|
|
60
|
+
callExpr.arguments.length === 1 &&
|
|
61
|
+
j.Identifier.check(callExpr.arguments[0]) &&
|
|
62
|
+
j.Identifier.check(executor.params[0]) &&
|
|
63
|
+
callExpr.arguments[0].name === executor.params[0].name
|
|
64
|
+
) {
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For functions with block body containing single resolve(expr) call
|
|
72
|
+
if (body.body.length === 1 && j.ExpressionStatement.check(body.body[0])) {
|
|
73
|
+
const expr = body.body[0].expression
|
|
74
|
+
if (
|
|
75
|
+
j.CallExpression.check(expr) &&
|
|
76
|
+
j.Identifier.check(expr.callee) &&
|
|
77
|
+
expr.callee.name === executor.params[0].name
|
|
78
|
+
) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false
|
|
84
|
+
})
|
|
85
|
+
.forEach((path) => {
|
|
86
|
+
const node = path.node
|
|
87
|
+
const executor = node.arguments[0]
|
|
88
|
+
const body = executor.body
|
|
89
|
+
const resolveParam = executor.params[0]
|
|
90
|
+
|
|
91
|
+
let expression
|
|
92
|
+
let tryArg
|
|
93
|
+
|
|
94
|
+
// Extract the expression
|
|
95
|
+
if (!j.BlockStatement.check(body)) {
|
|
96
|
+
// Arrow function with expression body: (resolve) => expr
|
|
97
|
+
expression = body
|
|
98
|
+
|
|
99
|
+
// Check if expression is a call where resolve is passed as the only argument
|
|
100
|
+
// e.g., (resolve) => setTimeout(resolve) should become Promise.try(setTimeout)
|
|
101
|
+
if (
|
|
102
|
+
j.CallExpression.check(expression) &&
|
|
103
|
+
expression.arguments.length === 1 &&
|
|
104
|
+
j.Identifier.check(expression.arguments[0]) &&
|
|
105
|
+
j.Identifier.check(resolveParam) &&
|
|
106
|
+
expression.arguments[0].name === resolveParam.name
|
|
107
|
+
) {
|
|
108
|
+
// Use the callee directly (e.g., setTimeout)
|
|
109
|
+
tryArg = expression.callee
|
|
110
|
+
}
|
|
111
|
+
// Check if expression is resolve(something)
|
|
112
|
+
else if (
|
|
113
|
+
j.CallExpression.check(expression) &&
|
|
114
|
+
j.Identifier.check(expression.callee) &&
|
|
115
|
+
j.Identifier.check(resolveParam) &&
|
|
116
|
+
expression.callee.name === resolveParam.name &&
|
|
117
|
+
expression.arguments.length > 0
|
|
118
|
+
) {
|
|
119
|
+
// Extract the argument from resolve(arg) and wrap in arrow function
|
|
120
|
+
expression = expression.arguments[0]
|
|
121
|
+
tryArg = j.arrowFunctionExpression([], expression)
|
|
122
|
+
}
|
|
123
|
+
// Note: No else needed - filter ensures only the above patterns reach here
|
|
124
|
+
} else if (body.body.length === 1 && j.ExpressionStatement.check(body.body[0])) {
|
|
125
|
+
// Block with resolve(expr) call
|
|
126
|
+
const callExpr = body.body[0].expression
|
|
127
|
+
if (j.CallExpression.check(callExpr) && callExpr.arguments.length > 0) {
|
|
128
|
+
expression = callExpr.arguments[0]
|
|
129
|
+
// Wrap expression in arrow function for Promise.try
|
|
130
|
+
tryArg = j.arrowFunctionExpression([], expression)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (tryArg) {
|
|
135
|
+
// Create Promise.try(fn)
|
|
136
|
+
const promiseTryCall = j.callExpression(
|
|
137
|
+
j.memberExpression(j.identifier("Promise"), j.identifier("try")),
|
|
138
|
+
[tryArg],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
j(path).replaceWith(promiseTryCall)
|
|
142
|
+
|
|
143
|
+
modified = true
|
|
144
|
+
if (node.loc) {
|
|
145
|
+
changes.push({
|
|
146
|
+
type: "promiseTry",
|
|
147
|
+
line: node.loc.start.line,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return { modified, changes }
|
|
154
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform var to const
|
|
3
|
+
*/
|
|
4
|
+
export function varToConst(j, root) {
|
|
5
|
+
let modified = false
|
|
6
|
+
const changes = []
|
|
7
|
+
|
|
8
|
+
root.find(j.VariableDeclaration, { kind: "var" }).forEach((path) => {
|
|
9
|
+
path.node.kind = "const"
|
|
10
|
+
modified = true
|
|
11
|
+
if (path.node.loc) {
|
|
12
|
+
changes.push({
|
|
13
|
+
type: "varToConst",
|
|
14
|
+
line: path.node.loc.start.line,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return { modified, changes }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Transform string concatenation to template literals
|
|
24
|
+
*/
|
|
25
|
+
export function concatToTemplateLiteral(j, root) {
|
|
26
|
+
let modified = false
|
|
27
|
+
const changes = []
|
|
28
|
+
|
|
29
|
+
root
|
|
30
|
+
.find(j.BinaryExpression, { operator: "+" })
|
|
31
|
+
.filter((path) => {
|
|
32
|
+
// Only transform if at least one operand is a string literal
|
|
33
|
+
const hasStringLiteral = (node) => {
|
|
34
|
+
if (
|
|
35
|
+
j.StringLiteral.check(node) ||
|
|
36
|
+
(j.Literal.check(node) && typeof node.value === "string")
|
|
37
|
+
) {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
if (j.BinaryExpression.check(node) && node.operator === "+") {
|
|
41
|
+
return hasStringLiteral(node.left) || hasStringLiteral(node.right)
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
return hasStringLiteral(path.node)
|
|
46
|
+
})
|
|
47
|
+
.forEach((path) => {
|
|
48
|
+
const parts = []
|
|
49
|
+
const expressions = []
|
|
50
|
+
|
|
51
|
+
const flatten = (node) => {
|
|
52
|
+
if (j.BinaryExpression.check(node) && node.operator === "+") {
|
|
53
|
+
flatten(node.left)
|
|
54
|
+
flatten(node.right)
|
|
55
|
+
} else if (
|
|
56
|
+
j.StringLiteral.check(node) ||
|
|
57
|
+
(j.Literal.check(node) && typeof node.value === "string")
|
|
58
|
+
) {
|
|
59
|
+
// Add string literal value
|
|
60
|
+
if (parts.length === 0 || expressions.length >= parts.length) {
|
|
61
|
+
parts.push(node.value)
|
|
62
|
+
} else {
|
|
63
|
+
parts[parts.length - 1] += node.value
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Add expression
|
|
67
|
+
if (parts.length === 0) {
|
|
68
|
+
parts.push("")
|
|
69
|
+
}
|
|
70
|
+
expressions.push(node)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
flatten(path.node)
|
|
75
|
+
|
|
76
|
+
// Ensure we have the right number of quasis (one more than expressions)
|
|
77
|
+
while (parts.length <= expressions.length) {
|
|
78
|
+
parts.push("")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create template literal
|
|
82
|
+
const quasis = parts.map((part, i) =>
|
|
83
|
+
j.templateElement({ raw: part, cooked: part }, i === parts.length - 1),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const templateLiteral = j.templateLiteral(quasis, expressions)
|
|
87
|
+
j(path).replaceWith(templateLiteral)
|
|
88
|
+
|
|
89
|
+
modified = true
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return { modified, changes }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Transform Object.assign({}, ...) to object spread
|
|
97
|
+
*/
|
|
98
|
+
export function objectAssignToSpread(j, root) {
|
|
99
|
+
let modified = false
|
|
100
|
+
const changes = []
|
|
101
|
+
|
|
102
|
+
root
|
|
103
|
+
.find(j.CallExpression, {
|
|
104
|
+
callee: {
|
|
105
|
+
type: "MemberExpression",
|
|
106
|
+
object: { name: "Object" },
|
|
107
|
+
property: { name: "assign" },
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
.filter((path) => {
|
|
111
|
+
// First argument must be empty object literal
|
|
112
|
+
const firstArg = path.node.arguments[0]
|
|
113
|
+
return j.ObjectExpression.check(firstArg) && firstArg.properties.length === 0
|
|
114
|
+
})
|
|
115
|
+
.forEach((path) => {
|
|
116
|
+
const spreadProperties = path.node.arguments
|
|
117
|
+
.slice(1)
|
|
118
|
+
.map((arg) => j.spreadElement(arg))
|
|
119
|
+
|
|
120
|
+
const objectExpression = j.objectExpression(spreadProperties)
|
|
121
|
+
j(path).replaceWith(objectExpression)
|
|
122
|
+
|
|
123
|
+
modified = true
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return { modified, changes }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Transform Array.from().forEach() to for...of
|
|
131
|
+
*/
|
|
132
|
+
export function arrayFromForEachToForOf(j, root) {
|
|
133
|
+
let modified = false
|
|
134
|
+
const changes = []
|
|
135
|
+
|
|
136
|
+
root
|
|
137
|
+
.find(j.CallExpression)
|
|
138
|
+
.filter((path) => {
|
|
139
|
+
const node = path.node
|
|
140
|
+
// Check if this is a forEach call
|
|
141
|
+
if (
|
|
142
|
+
!j.MemberExpression.check(node.callee) ||
|
|
143
|
+
!j.Identifier.check(node.callee.property) ||
|
|
144
|
+
node.callee.property.name !== "forEach"
|
|
145
|
+
) {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if the object is Array.from()
|
|
150
|
+
const object = node.callee.object
|
|
151
|
+
if (
|
|
152
|
+
!j.CallExpression.check(object) ||
|
|
153
|
+
!j.MemberExpression.check(object.callee) ||
|
|
154
|
+
!j.Identifier.check(object.callee.object) ||
|
|
155
|
+
object.callee.object.name !== "Array" ||
|
|
156
|
+
!j.Identifier.check(object.callee.property) ||
|
|
157
|
+
object.callee.property.name !== "from"
|
|
158
|
+
) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return true
|
|
163
|
+
})
|
|
164
|
+
.forEach((path) => {
|
|
165
|
+
const node = path.node
|
|
166
|
+
const iterable = node.callee.object.arguments[0]
|
|
167
|
+
const callback = node.arguments[0]
|
|
168
|
+
|
|
169
|
+
// Only transform if callback is a function
|
|
170
|
+
if (
|
|
171
|
+
callback &&
|
|
172
|
+
(j.ArrowFunctionExpression.check(callback) ||
|
|
173
|
+
j.FunctionExpression.check(callback))
|
|
174
|
+
) {
|
|
175
|
+
// Only transform if:
|
|
176
|
+
// 1. Callback has exactly 1 parameter (element only), OR
|
|
177
|
+
// 2. Callback has 2+ params AND first param is a destructuring pattern (e.g., [key, value])
|
|
178
|
+
// This handles cases like Array.from(Object.entries(obj)).forEach(([k, v]) => ...)
|
|
179
|
+
const params = callback.params
|
|
180
|
+
const canTransform =
|
|
181
|
+
params.length === 1 || (params.length >= 2 && j.ArrayPattern.check(params[0]))
|
|
182
|
+
|
|
183
|
+
if (canTransform) {
|
|
184
|
+
const itemParam = callback.params[0]
|
|
185
|
+
const body = callback.body
|
|
186
|
+
|
|
187
|
+
// Create for...of loop
|
|
188
|
+
const forOfLoop = j.forOfStatement(
|
|
189
|
+
j.variableDeclaration("const", [j.variableDeclarator(itemParam)]),
|
|
190
|
+
iterable,
|
|
191
|
+
j.BlockStatement.check(body)
|
|
192
|
+
? body
|
|
193
|
+
: j.blockStatement([j.expressionStatement(body)]),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// Replace the expression statement containing the forEach call
|
|
197
|
+
const statement = path.parent
|
|
198
|
+
if (j.ExpressionStatement.check(statement.node)) {
|
|
199
|
+
j(statement).replaceWith(forOfLoop)
|
|
200
|
+
|
|
201
|
+
modified = true
|
|
202
|
+
if (node.loc) {
|
|
203
|
+
changes.push({
|
|
204
|
+
type: "arrayFromForEachToForOf",
|
|
205
|
+
line: node.loc.start.line,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return { modified, changes }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Transform for...of Object.keys() loops to for...in
|
|
218
|
+
*/
|
|
219
|
+
export function forOfKeysToForIn(j, root) {
|
|
220
|
+
let modified = false
|
|
221
|
+
const changes = []
|
|
222
|
+
|
|
223
|
+
root
|
|
224
|
+
.find(j.ForOfStatement)
|
|
225
|
+
.filter((path) => {
|
|
226
|
+
const node = path.node
|
|
227
|
+
const right = node.right
|
|
228
|
+
|
|
229
|
+
// Check if iterating over Object.keys() call
|
|
230
|
+
if (
|
|
231
|
+
j.CallExpression.check(right) &&
|
|
232
|
+
j.MemberExpression.check(right.callee) &&
|
|
233
|
+
j.Identifier.check(right.callee.object) &&
|
|
234
|
+
right.callee.object.name === "Object" &&
|
|
235
|
+
j.Identifier.check(right.callee.property) &&
|
|
236
|
+
right.callee.property.name === "keys" &&
|
|
237
|
+
right.arguments.length === 1
|
|
238
|
+
) {
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false
|
|
243
|
+
})
|
|
244
|
+
.forEach((path) => {
|
|
245
|
+
const node = path.node
|
|
246
|
+
const left = node.left
|
|
247
|
+
const objectArg = node.right.arguments[0]
|
|
248
|
+
const body = node.body
|
|
249
|
+
|
|
250
|
+
// Create for...in loop
|
|
251
|
+
const forInLoop = j.forInStatement(left, objectArg, body)
|
|
252
|
+
|
|
253
|
+
j(path).replaceWith(forInLoop)
|
|
254
|
+
|
|
255
|
+
modified = true
|
|
256
|
+
if (node.loc) {
|
|
257
|
+
changes.push({
|
|
258
|
+
type: "forOfKeysToForIn",
|
|
259
|
+
line: node.loc.start.line,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return { modified, changes }
|
|
265
|
+
}
|