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 ADDED
@@ -0,0 +1,3 @@
1
+ When writing code, you MUST ALWAYS follow the [naming-things](https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md) guidlines.
2
+
3
+ All code must be fully tested with a 100% coverage. Unreachable code must be removed.
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
- console.log(`✗ ${filePath}`)
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].push(change.line)
108
+ changesByType[change.type]++
111
109
  }
110
+ }
112
111
 
113
- for (const [type, lines] of Object.entries(changesByType)) {
114
- const uniqueLines = [...new Set(lines)].sort((a, b) => a - b)
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
- console.log(
120
- ` - ${displayName} (line${uniqueLines.length > 1 ? "s" : ""}: ${uniqueLines.join(", ")})`,
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(`✓ Upgraded: ${filePath}`)
131
+ console.log(`✓ ${filePath}`)
130
132
  } else {
131
- console.log(` ✓ Changes written to file`)
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(` No changes: ${filePath}`)
140
+ console.log(` ${filePath}`)
139
141
  }
140
- return false
142
+ return { modified: false, changes: [] }
141
143
  }
142
144
  } catch (error) {
143
- console.error(`Error processing ${filePath}: ${error.message}`)
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
- if (processFile(file, options)) {
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
- console.log("")
167
- if (modifiedCount > 0) {
168
- if (options.write && options.check) {
169
- console.log(`Summary: ${modifiedCount} file(s) upgraded`)
170
- } else if (options.write) {
171
- console.log(`Summary: ${modifiedCount} file(s) upgraded`)
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(`Summary: ${modifiedCount} file(s) need upgrading`)
185
+ console.log("All files are up to date")
174
186
  }
175
187
  } else {
176
- console.log("Summary: All files are already modern")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.1.0",
3
+ "version": "2025.2.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
+ }