esupgrade 2025.1.0 → 2025.2.0
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/AGENTS.md +3 -0
- package/README.md +48 -0
- package/bin/esupgrade.js +47 -30
- package/package.json +1 -1
- package/src/widelyAvailable.js +404 -0
- package/tests/cli.test.js +13 -17
- package/tests/widelyAvailable.test.js +1031 -0
package/AGENTS.md
ADDED
package/README.md
CHANGED
|
@@ -93,6 +93,21 @@ For more information about Baseline browser support, visit [web.dev/baseline][ba
|
|
|
93
93
|
+const message = `You have ${count} items`;
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
#### Traditional `for` loops → [`for...of` loops][mdn-for-of]
|
|
97
|
+
|
|
98
|
+
```diff
|
|
99
|
+
-for (let i = 0; i < items.length; i++) {
|
|
100
|
+
- const item = items[i];
|
|
101
|
+
- console.log(item);
|
|
102
|
+
-}
|
|
103
|
+
+for (const item of items) {
|
|
104
|
+
+ console.log(item);
|
|
105
|
+
+}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> [!NOTE]
|
|
109
|
+
> Transformations are limited to loops that start at 0, increment by 1, and where the index variable is not used in the loop body.
|
|
110
|
+
|
|
96
111
|
#### `Array.from().forEach()` → [`for...of` loops][mdn-for-of]
|
|
97
112
|
|
|
98
113
|
```diff
|
|
@@ -104,6 +119,29 @@ For more information about Baseline browser support, visit [web.dev/baseline][ba
|
|
|
104
119
|
+}
|
|
105
120
|
```
|
|
106
121
|
|
|
122
|
+
#### DOM `forEach()` → [`for...of` loops][mdn-for-of]
|
|
123
|
+
|
|
124
|
+
```diff
|
|
125
|
+
-document.querySelectorAll('.item').forEach(item => {
|
|
126
|
+
- item.classList.add('active');
|
|
127
|
+
-});
|
|
128
|
+
+for (const item of document.querySelectorAll('.item')) {
|
|
129
|
+
+ item.classList.add('active');
|
|
130
|
+
+}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Supports:
|
|
134
|
+
|
|
135
|
+
- `document.querySelectorAll()`
|
|
136
|
+
- `document.getElementsByTagName()`
|
|
137
|
+
- `document.getElementsByClassName()`
|
|
138
|
+
- `document.getElementsByName()`
|
|
139
|
+
- `window.frames`
|
|
140
|
+
|
|
141
|
+
> [!NOTE]
|
|
142
|
+
> Transformations limited to inline arrow or function expressions with block statement bodies.
|
|
143
|
+
> Callbacks with index parameters or expression bodies are not transformed.
|
|
144
|
+
|
|
107
145
|
#### `Array.from()` → [Array spread [...]][mdn-spread]
|
|
108
146
|
|
|
109
147
|
```diff
|
|
@@ -136,6 +174,15 @@ For more information about Baseline browser support, visit [web.dev/baseline][ba
|
|
|
136
174
|
+const withItem = [...array, item];
|
|
137
175
|
```
|
|
138
176
|
|
|
177
|
+
#### `Math.pow()` → [Exponentiation operator \*\*][mdn-exponentiation]
|
|
178
|
+
|
|
179
|
+
```diff
|
|
180
|
+
-const result = Math.pow(2, 3);
|
|
181
|
+
-const area = Math.PI * Math.pow(radius, 2);
|
|
182
|
+
+const result = 2 ** 3;
|
|
183
|
+
+const area = Math.PI * radius ** 2;
|
|
184
|
+
```
|
|
185
|
+
|
|
139
186
|
#### Function expressions → [Arrow functions][mdn-arrow-functions]
|
|
140
187
|
|
|
141
188
|
```diff
|
|
@@ -174,6 +221,7 @@ For more information about Baseline browser support, visit [web.dev/baseline][ba
|
|
|
174
221
|
[baseline]: https://web.dev/baseline/
|
|
175
222
|
[mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
|
176
223
|
[mdn-const]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
|
|
224
|
+
[mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
|
|
177
225
|
[mdn-for-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
|
|
178
226
|
[mdn-let]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
|
|
179
227
|
[mdn-promise-try]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try
|
package/bin/esupgrade.js
CHANGED
|
@@ -98,50 +98,52 @@ function processFile(filePath, options) {
|
|
|
98
98
|
|
|
99
99
|
if (result.modified) {
|
|
100
100
|
if (options.check) {
|
|
101
|
-
|
|
101
|
+
// Group changes by type for summary
|
|
102
|
+
const changesByType = {}
|
|
102
103
|
if (result.changes && result.changes.length > 0) {
|
|
103
|
-
// Group changes by type
|
|
104
|
-
const changesByType = {}
|
|
105
|
-
|
|
106
104
|
for (const change of result.changes) {
|
|
107
105
|
if (!changesByType[change.type]) {
|
|
108
|
-
changesByType[change.type] =
|
|
106
|
+
changesByType[change.type] = 0
|
|
109
107
|
}
|
|
110
|
-
changesByType[change.type]
|
|
108
|
+
changesByType[change.type]++
|
|
111
109
|
}
|
|
110
|
+
}
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const transformations = Object.keys(changesByType)
|
|
113
|
+
.map((type) => {
|
|
115
114
|
const displayName = type
|
|
116
115
|
.replace(/([A-Z])/g, " $1")
|
|
117
116
|
.trim()
|
|
118
117
|
.toLowerCase()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
return displayName
|
|
119
|
+
})
|
|
120
|
+
.join(", ")
|
|
121
|
+
|
|
122
|
+
console.log(`✗ ${filePath}`)
|
|
123
|
+
if (transformations) {
|
|
124
|
+
console.log(` ${transformations}`)
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
if (options.write) {
|
|
127
129
|
fs.writeFileSync(filePath, result.code, "utf8")
|
|
128
130
|
if (!options.check) {
|
|
129
|
-
console.log(`✓
|
|
131
|
+
console.log(`✓ ${filePath}`)
|
|
130
132
|
} else {
|
|
131
|
-
console.log(` ✓
|
|
133
|
+
console.log(` ✓ written`)
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
return true
|
|
137
|
+
return { modified: true, changes: result.changes }
|
|
136
138
|
} else {
|
|
137
139
|
if (!options.check) {
|
|
138
|
-
console.log(`
|
|
140
|
+
console.log(` ${filePath}`)
|
|
139
141
|
}
|
|
140
|
-
return false
|
|
142
|
+
return { modified: false, changes: [] }
|
|
141
143
|
}
|
|
142
144
|
} catch (error) {
|
|
143
|
-
console.error(
|
|
144
|
-
return false
|
|
145
|
+
console.error(`✗ Error: ${filePath}: ${error.message}`)
|
|
146
|
+
return { modified: false, changes: [] }
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
@@ -153,27 +155,42 @@ function processFiles(patterns, options) {
|
|
|
153
155
|
process.exit(0)
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
console.log(`Processing ${files.length} file(s) with baseline: ${options.baseline}\n`)
|
|
157
|
-
|
|
158
158
|
let modifiedCount = 0
|
|
159
|
+
const allChanges = []
|
|
160
|
+
|
|
159
161
|
for (const file of files) {
|
|
160
|
-
|
|
162
|
+
const result = processFile(file, options)
|
|
163
|
+
if (result.modified) {
|
|
161
164
|
modifiedCount++
|
|
165
|
+
allChanges.push(...result.changes)
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
// Summary
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
if (options.check) {
|
|
171
|
+
console.log("")
|
|
172
|
+
if (modifiedCount > 0) {
|
|
173
|
+
// Count unique transformation types
|
|
174
|
+
const transformTypes = new Set(allChanges.map((c) => c.type))
|
|
175
|
+
const typeCount = transformTypes.size
|
|
176
|
+
const totalChanges = allChanges.length
|
|
177
|
+
|
|
178
|
+
console.log(
|
|
179
|
+
`${modifiedCount} file(s) need upgrading (${totalChanges} change${totalChanges !== 1 ? "s" : ""}, ${typeCount} type${typeCount !== 1 ? "s" : ""})`,
|
|
180
|
+
)
|
|
181
|
+
if (options.write) {
|
|
182
|
+
console.log("Changes have been written")
|
|
183
|
+
}
|
|
172
184
|
} else {
|
|
173
|
-
console.log(
|
|
185
|
+
console.log("All files are up to date")
|
|
174
186
|
}
|
|
175
187
|
} else {
|
|
176
|
-
console.log("
|
|
188
|
+
console.log("")
|
|
189
|
+
if (modifiedCount > 0) {
|
|
190
|
+
console.log(`✓ ${modifiedCount} file(s) upgraded`)
|
|
191
|
+
} else {
|
|
192
|
+
console.log("All files are up to date")
|
|
193
|
+
}
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
// Exit with code 1 if --check specified and there were changes
|
package/package.json
CHANGED
package/src/widelyAvailable.js
CHANGED
|
@@ -335,3 +335,407 @@ export function arrayFromToSpread(j, root) {
|
|
|
335
335
|
|
|
336
336
|
return { modified, changes }
|
|
337
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Transform Math.pow() to exponentiation operator (**)
|
|
341
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
|
|
342
|
+
*/
|
|
343
|
+
export function mathPowToExponentiation(j, root) {
|
|
344
|
+
let modified = false
|
|
345
|
+
const changes = []
|
|
346
|
+
|
|
347
|
+
root
|
|
348
|
+
.find(j.CallExpression, {
|
|
349
|
+
callee: {
|
|
350
|
+
type: "MemberExpression",
|
|
351
|
+
object: { name: "Math" },
|
|
352
|
+
property: { name: "pow" },
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
.filter((path) => {
|
|
356
|
+
// Must have exactly 2 arguments (base and exponent)
|
|
357
|
+
return path.node.arguments.length === 2
|
|
358
|
+
})
|
|
359
|
+
.forEach((path) => {
|
|
360
|
+
const node = path.node
|
|
361
|
+
const [base, exponent] = node.arguments
|
|
362
|
+
|
|
363
|
+
// Create exponentiation expression
|
|
364
|
+
const expExpression = j.binaryExpression("**", base, exponent)
|
|
365
|
+
|
|
366
|
+
j(path).replaceWith(expExpression)
|
|
367
|
+
|
|
368
|
+
modified = true
|
|
369
|
+
if (node.loc) {
|
|
370
|
+
changes.push({
|
|
371
|
+
type: "mathPowToExponentiation",
|
|
372
|
+
line: node.loc.start.line,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
return { modified, changes }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Transform traditional for loops to for...of where safe
|
|
382
|
+
* Converts: for (let i = 0; i < arr.length; i++) { const item = arr[i]; ... }
|
|
383
|
+
* To: for (const item of arr) { ... }
|
|
384
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
|
|
385
|
+
*/
|
|
386
|
+
export function forLoopToForOf(j, root) {
|
|
387
|
+
let modified = false
|
|
388
|
+
const changes = []
|
|
389
|
+
|
|
390
|
+
root
|
|
391
|
+
.find(j.ForStatement)
|
|
392
|
+
.filter((path) => {
|
|
393
|
+
const node = path.node
|
|
394
|
+
|
|
395
|
+
// Check init: must be let/const i = 0
|
|
396
|
+
if (!j.VariableDeclaration.check(node.init)) {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
if (node.init.declarations.length !== 1) {
|
|
400
|
+
return false
|
|
401
|
+
}
|
|
402
|
+
const initDeclarator = node.init.declarations[0]
|
|
403
|
+
if (!j.Identifier.check(initDeclarator.id)) {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
const indexVar = initDeclarator.id.name
|
|
407
|
+
if (!j.Literal.check(initDeclarator.init) || initDeclarator.init.value !== 0) {
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check test: must be i < arr.length
|
|
412
|
+
if (!j.BinaryExpression.check(node.test)) {
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
if (node.test.operator !== "<") {
|
|
416
|
+
return false
|
|
417
|
+
}
|
|
418
|
+
if (!j.Identifier.check(node.test.left) || node.test.left.name !== indexVar) {
|
|
419
|
+
return false
|
|
420
|
+
}
|
|
421
|
+
if (!j.MemberExpression.check(node.test.right)) {
|
|
422
|
+
return false
|
|
423
|
+
}
|
|
424
|
+
if (
|
|
425
|
+
!j.Identifier.check(node.test.right.property) ||
|
|
426
|
+
node.test.right.property.name !== "length"
|
|
427
|
+
) {
|
|
428
|
+
return false
|
|
429
|
+
}
|
|
430
|
+
if (!j.Identifier.check(node.test.right.object)) {
|
|
431
|
+
return false
|
|
432
|
+
}
|
|
433
|
+
const arrayVar = node.test.right.object.name
|
|
434
|
+
|
|
435
|
+
// Check update: must be i++ or ++i
|
|
436
|
+
if (j.UpdateExpression.check(node.update)) {
|
|
437
|
+
if (
|
|
438
|
+
!j.Identifier.check(node.update.argument) ||
|
|
439
|
+
node.update.argument.name !== indexVar ||
|
|
440
|
+
node.update.operator !== "++"
|
|
441
|
+
) {
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
return false
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Check body: must be a block statement
|
|
449
|
+
if (!j.BlockStatement.check(node.body)) {
|
|
450
|
+
return false
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Look for first statement that assigns arr[i] to a variable
|
|
454
|
+
if (node.body.body.length === 0) {
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const firstStmt = node.body.body[0]
|
|
459
|
+
if (!j.VariableDeclaration.check(firstStmt)) {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
if (firstStmt.declarations.length !== 1) {
|
|
463
|
+
return false
|
|
464
|
+
}
|
|
465
|
+
const varDeclarator = firstStmt.declarations[0]
|
|
466
|
+
if (!j.Identifier.check(varDeclarator.id)) {
|
|
467
|
+
return false
|
|
468
|
+
}
|
|
469
|
+
if (!j.MemberExpression.check(varDeclarator.init)) {
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
472
|
+
if (
|
|
473
|
+
!j.Identifier.check(varDeclarator.init.object) ||
|
|
474
|
+
varDeclarator.init.object.name !== arrayVar
|
|
475
|
+
) {
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
if (
|
|
479
|
+
!j.Identifier.check(varDeclarator.init.property) ||
|
|
480
|
+
varDeclarator.init.property.name !== indexVar ||
|
|
481
|
+
varDeclarator.init.computed !== true
|
|
482
|
+
) {
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check that the index variable is not used elsewhere in the body
|
|
487
|
+
const bodyWithoutFirst = node.body.body.slice(1)
|
|
488
|
+
let indexVarUsed = false
|
|
489
|
+
|
|
490
|
+
// Recursively check if identifier is used in AST nodes
|
|
491
|
+
const checkNode = (astNode) => {
|
|
492
|
+
if (!astNode || typeof astNode !== "object") return
|
|
493
|
+
|
|
494
|
+
if (astNode.type === "Identifier" && astNode.name === indexVar) {
|
|
495
|
+
indexVarUsed = true
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Traverse all properties
|
|
500
|
+
for (const key in astNode) {
|
|
501
|
+
if (
|
|
502
|
+
key === "loc" ||
|
|
503
|
+
key === "start" ||
|
|
504
|
+
key === "end" ||
|
|
505
|
+
key === "tokens" ||
|
|
506
|
+
key === "comments"
|
|
507
|
+
)
|
|
508
|
+
continue
|
|
509
|
+
const value = astNode[key]
|
|
510
|
+
if (Array.isArray(value)) {
|
|
511
|
+
value.forEach(checkNode)
|
|
512
|
+
} else if (value && typeof value === "object") {
|
|
513
|
+
checkNode(value)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
bodyWithoutFirst.forEach(checkNode)
|
|
519
|
+
|
|
520
|
+
if (indexVarUsed) {
|
|
521
|
+
return false
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return true
|
|
525
|
+
})
|
|
526
|
+
.forEach((path) => {
|
|
527
|
+
const node = path.node
|
|
528
|
+
const arrayVar = node.test.right.object.name
|
|
529
|
+
const itemVar = node.body.body[0].declarations[0].id.name
|
|
530
|
+
const itemKind = node.body.body[0].kind
|
|
531
|
+
|
|
532
|
+
// Create new body without the first declaration
|
|
533
|
+
const newBody = j.blockStatement(node.body.body.slice(1))
|
|
534
|
+
|
|
535
|
+
// Create for...of loop
|
|
536
|
+
const forOfLoop = j.forOfStatement(
|
|
537
|
+
j.variableDeclaration(itemKind, [j.variableDeclarator(j.identifier(itemVar))]),
|
|
538
|
+
j.identifier(arrayVar),
|
|
539
|
+
newBody,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
j(path).replaceWith(forOfLoop)
|
|
543
|
+
|
|
544
|
+
modified = true
|
|
545
|
+
if (node.loc) {
|
|
546
|
+
changes.push({
|
|
547
|
+
type: "forLoopToForOf",
|
|
548
|
+
line: node.loc.start.line,
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
return { modified, changes }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Transform iterables' forEach() to for...of loop
|
|
558
|
+
* Handles DOM APIs like querySelectorAll, getElementsBy*, etc. and other known iterables
|
|
559
|
+
* Only transforms when forEach callback is declared inline with a function body (block statement)
|
|
560
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
|
|
561
|
+
*/
|
|
562
|
+
export function iterableForEachToForOf(j, root) {
|
|
563
|
+
let modified = false
|
|
564
|
+
const changes = []
|
|
565
|
+
|
|
566
|
+
// Define known iterable-returning methods by their object/context
|
|
567
|
+
const knownIterableMethods = {
|
|
568
|
+
document: [
|
|
569
|
+
"querySelectorAll",
|
|
570
|
+
"getElementsByTagName",
|
|
571
|
+
"getElementsByClassName",
|
|
572
|
+
"getElementsByName",
|
|
573
|
+
],
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Define known iterable properties
|
|
577
|
+
const knownIterableProperties = {
|
|
578
|
+
window: ["frames"],
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
root
|
|
582
|
+
.find(j.CallExpression)
|
|
583
|
+
.filter((path) => {
|
|
584
|
+
const node = path.node
|
|
585
|
+
// Check if this is a forEach call
|
|
586
|
+
if (
|
|
587
|
+
!j.MemberExpression.check(node.callee) ||
|
|
588
|
+
!j.Identifier.check(node.callee.property) ||
|
|
589
|
+
node.callee.property.name !== "forEach"
|
|
590
|
+
) {
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const object = node.callee.object
|
|
595
|
+
|
|
596
|
+
// Check if this is a property access pattern like window.frames
|
|
597
|
+
if (j.MemberExpression.check(object) && !j.CallExpression.check(object)) {
|
|
598
|
+
const objectName = j.Identifier.check(object.object) ? object.object.name : null
|
|
599
|
+
const propertyName = j.Identifier.check(object.property)
|
|
600
|
+
? object.property.name
|
|
601
|
+
: null
|
|
602
|
+
|
|
603
|
+
if (
|
|
604
|
+
objectName &&
|
|
605
|
+
propertyName &&
|
|
606
|
+
knownIterableProperties[objectName] &&
|
|
607
|
+
knownIterableProperties[objectName].includes(propertyName)
|
|
608
|
+
) {
|
|
609
|
+
// This is a valid iterable property like window.frames - continue to callback check
|
|
610
|
+
} else {
|
|
611
|
+
// Not a known iterable property
|
|
612
|
+
return false
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Check for method call patterns like document.querySelectorAll()
|
|
616
|
+
else if (j.CallExpression.check(object)) {
|
|
617
|
+
// Check if it's a member expression (e.g., document.querySelectorAll)
|
|
618
|
+
if (!j.MemberExpression.check(object.callee)) {
|
|
619
|
+
return false
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Get the method name
|
|
623
|
+
const methodName = j.Identifier.check(object.callee.property)
|
|
624
|
+
? object.callee.property.name
|
|
625
|
+
: null
|
|
626
|
+
|
|
627
|
+
if (!methodName) {
|
|
628
|
+
return false
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Verify the object is document only
|
|
632
|
+
const callerObject = object.callee.object
|
|
633
|
+
if (j.Identifier.check(callerObject)) {
|
|
634
|
+
const objectName = callerObject.name
|
|
635
|
+
// Only allow document
|
|
636
|
+
if (objectName !== "document") {
|
|
637
|
+
return false
|
|
638
|
+
}
|
|
639
|
+
// Verify method belongs to document
|
|
640
|
+
if (!knownIterableMethods.document.includes(methodName)) {
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// Handle cases like document.getElementById('x').querySelectorAll()
|
|
645
|
+
// Only allow these from document-originating chains
|
|
646
|
+
else if (
|
|
647
|
+
j.MemberExpression.check(callerObject) ||
|
|
648
|
+
j.CallExpression.check(callerObject)
|
|
649
|
+
) {
|
|
650
|
+
// Check if this eventually chains from document
|
|
651
|
+
const isFromDocument = (node) => {
|
|
652
|
+
if (j.Identifier.check(node)) {
|
|
653
|
+
return node.name === "document"
|
|
654
|
+
}
|
|
655
|
+
if (j.MemberExpression.check(node)) {
|
|
656
|
+
return isFromDocument(node.object)
|
|
657
|
+
}
|
|
658
|
+
if (j.CallExpression.check(node)) {
|
|
659
|
+
if (j.MemberExpression.check(node.callee)) {
|
|
660
|
+
return isFromDocument(node.callee.object)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return false
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!isFromDocument(callerObject)) {
|
|
667
|
+
return false
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Verify the method is valid for document
|
|
671
|
+
if (!knownIterableMethods.document.includes(methodName)) {
|
|
672
|
+
return false
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
return false
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
return false
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Check that forEach has a callback argument
|
|
682
|
+
if (node.arguments.length === 0) {
|
|
683
|
+
return false
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const callback = node.arguments[0]
|
|
687
|
+
// Only transform if callback is an inline function (arrow or function expression)
|
|
688
|
+
if (
|
|
689
|
+
!j.ArrowFunctionExpression.check(callback) &&
|
|
690
|
+
!j.FunctionExpression.check(callback)
|
|
691
|
+
) {
|
|
692
|
+
return false
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Only transform if the callback has a block statement body (with braces)
|
|
696
|
+
// Arrow functions with expression bodies (e.g., item => item.value) should NOT be transformed
|
|
697
|
+
if (!j.BlockStatement.check(callback.body)) {
|
|
698
|
+
return false
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Only transform if callback uses only the first parameter (element)
|
|
702
|
+
// Don't transform if it uses index or array parameters
|
|
703
|
+
const params = callback.params
|
|
704
|
+
if (params.length !== 1) {
|
|
705
|
+
return false
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return true
|
|
709
|
+
})
|
|
710
|
+
.forEach((path) => {
|
|
711
|
+
const node = path.node
|
|
712
|
+
const iterable = node.callee.object
|
|
713
|
+
const callback = node.arguments[0]
|
|
714
|
+
|
|
715
|
+
const itemParam = callback.params[0]
|
|
716
|
+
const body = callback.body
|
|
717
|
+
|
|
718
|
+
// Create for...of loop
|
|
719
|
+
const forOfLoop = j.forOfStatement(
|
|
720
|
+
j.variableDeclaration("const", [j.variableDeclarator(itemParam)]),
|
|
721
|
+
iterable,
|
|
722
|
+
body,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
// Replace the expression statement containing the forEach call
|
|
726
|
+
const statement = path.parent
|
|
727
|
+
if (j.ExpressionStatement.check(statement.node)) {
|
|
728
|
+
j(statement).replaceWith(forOfLoop)
|
|
729
|
+
|
|
730
|
+
modified = true
|
|
731
|
+
if (node.loc) {
|
|
732
|
+
changes.push({
|
|
733
|
+
type: "iterableForEachToForOf",
|
|
734
|
+
line: node.loc.start.line,
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
return { modified, changes }
|
|
741
|
+
}
|