esupgrade 2025.1.0 → 2025.2.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.
@@ -51,26 +51,83 @@ export function concatToTemplateLiteral(j, root) {
51
51
  const parts = []
52
52
  const expressions = []
53
53
 
54
- const flatten = (node) => {
55
- if (j.BinaryExpression.check(node) && node.operator === "+") {
56
- flatten(node.left)
57
- flatten(node.right)
58
- } else if (
54
+ // Helper to check if a node is a string literal
55
+ const isStringLiteral = (node) => {
56
+ return (
59
57
  j.StringLiteral.check(node) ||
60
58
  (j.Literal.check(node) && typeof node.value === "string")
61
- ) {
62
- // Add string literal value
63
- if (parts.length === 0 || expressions.length >= parts.length) {
64
- parts.push(node.value)
65
- } else {
66
- parts[parts.length - 1] += node.value
67
- }
59
+ )
60
+ }
61
+
62
+ // Helper to check if a node contains any string literal
63
+ const containsStringLiteral = (node) => {
64
+ if (isStringLiteral(node)) return true
65
+ if (j.BinaryExpression.check(node) && node.operator === "+") {
66
+ return containsStringLiteral(node.left) || containsStringLiteral(node.right)
67
+ }
68
+ return false
69
+ }
70
+
71
+ const addStringPart = (value) => {
72
+ if (parts.length === 0 || expressions.length >= parts.length) {
73
+ parts.push(value)
68
74
  } else {
69
- // Add expression
70
- if (parts.length === 0) {
71
- parts.push("")
75
+ parts[parts.length - 1] += value
76
+ }
77
+ }
78
+
79
+ const addExpression = (expr) => {
80
+ if (parts.length === 0) {
81
+ parts.push("")
82
+ }
83
+ expressions.push(expr)
84
+ }
85
+
86
+ const flatten = (node, stringContext = false) => {
87
+ // Note: node is always a BinaryExpression when called, as non-BinaryExpression
88
+ // nodes are handled inline before recursing into flatten
89
+ if (j.BinaryExpression.check(node) && node.operator === "+") {
90
+ // Check if this entire binary expression contains any string literal
91
+ const hasString = containsStringLiteral(node)
92
+
93
+ if (!hasString && !stringContext) {
94
+ // This is pure numeric addition (no strings anywhere), keep as expression
95
+ addExpression(node)
96
+ } else {
97
+ // This binary expression is part of string concatenation
98
+ // Check each operand
99
+ const leftHasString = containsStringLiteral(node.left)
100
+
101
+ // Process left side
102
+ if (j.BinaryExpression.check(node.left) && node.left.operator === "+") {
103
+ // Left is also a + expression - recurse
104
+ flatten(node.left, stringContext)
105
+ } else if (isStringLiteral(node.left)) {
106
+ // Left is a string literal
107
+ addStringPart(node.left.value)
108
+ } else {
109
+ // Left is some other expression
110
+ addExpression(node.left)
111
+ }
112
+
113
+ // Process right side - it's in string context if left had a string
114
+ const rightInStringContext = stringContext || leftHasString
115
+ if (j.BinaryExpression.check(node.right) && node.right.operator === "+") {
116
+ // If right is a + expression with no strings and we're in string context, keep it as a unit
117
+ if (!containsStringLiteral(node.right) && rightInStringContext) {
118
+ addExpression(node.right)
119
+ } else {
120
+ // Right has strings or we need to flatten it
121
+ flatten(node.right, rightInStringContext)
122
+ }
123
+ } else if (isStringLiteral(node.right)) {
124
+ // Right is a string literal
125
+ addStringPart(node.right.value)
126
+ } else {
127
+ // Right is some other expression
128
+ addExpression(node.right)
129
+ }
72
130
  }
73
- expressions.push(node)
74
131
  }
75
132
  }
76
133
 
@@ -335,3 +392,407 @@ export function arrayFromToSpread(j, root) {
335
392
 
336
393
  return { modified, changes }
337
394
  }
395
+
396
+ /**
397
+ * Transform Math.pow() to exponentiation operator (**)
398
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
399
+ */
400
+ export function mathPowToExponentiation(j, root) {
401
+ let modified = false
402
+ const changes = []
403
+
404
+ root
405
+ .find(j.CallExpression, {
406
+ callee: {
407
+ type: "MemberExpression",
408
+ object: { name: "Math" },
409
+ property: { name: "pow" },
410
+ },
411
+ })
412
+ .filter((path) => {
413
+ // Must have exactly 2 arguments (base and exponent)
414
+ return path.node.arguments.length === 2
415
+ })
416
+ .forEach((path) => {
417
+ const node = path.node
418
+ const [base, exponent] = node.arguments
419
+
420
+ // Create exponentiation expression
421
+ const expExpression = j.binaryExpression("**", base, exponent)
422
+
423
+ j(path).replaceWith(expExpression)
424
+
425
+ modified = true
426
+ if (node.loc) {
427
+ changes.push({
428
+ type: "mathPowToExponentiation",
429
+ line: node.loc.start.line,
430
+ })
431
+ }
432
+ })
433
+
434
+ return { modified, changes }
435
+ }
436
+
437
+ /**
438
+ * Transform traditional for loops to for...of where safe
439
+ * Converts: for (let i = 0; i < arr.length; i++) { const item = arr[i]; ... }
440
+ * To: for (const item of arr) { ... }
441
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
442
+ */
443
+ export function forLoopToForOf(j, root) {
444
+ let modified = false
445
+ const changes = []
446
+
447
+ root
448
+ .find(j.ForStatement)
449
+ .filter((path) => {
450
+ const node = path.node
451
+
452
+ // Check init: must be let/const i = 0
453
+ if (!j.VariableDeclaration.check(node.init)) {
454
+ return false
455
+ }
456
+ if (node.init.declarations.length !== 1) {
457
+ return false
458
+ }
459
+ const initDeclarator = node.init.declarations[0]
460
+ if (!j.Identifier.check(initDeclarator.id)) {
461
+ return false
462
+ }
463
+ const indexVar = initDeclarator.id.name
464
+ if (!j.Literal.check(initDeclarator.init) || initDeclarator.init.value !== 0) {
465
+ return false
466
+ }
467
+
468
+ // Check test: must be i < arr.length
469
+ if (!j.BinaryExpression.check(node.test)) {
470
+ return false
471
+ }
472
+ if (node.test.operator !== "<") {
473
+ return false
474
+ }
475
+ if (!j.Identifier.check(node.test.left) || node.test.left.name !== indexVar) {
476
+ return false
477
+ }
478
+ if (!j.MemberExpression.check(node.test.right)) {
479
+ return false
480
+ }
481
+ if (
482
+ !j.Identifier.check(node.test.right.property) ||
483
+ node.test.right.property.name !== "length"
484
+ ) {
485
+ return false
486
+ }
487
+ if (!j.Identifier.check(node.test.right.object)) {
488
+ return false
489
+ }
490
+ const arrayVar = node.test.right.object.name
491
+
492
+ // Check update: must be i++ or ++i
493
+ if (j.UpdateExpression.check(node.update)) {
494
+ if (
495
+ !j.Identifier.check(node.update.argument) ||
496
+ node.update.argument.name !== indexVar ||
497
+ node.update.operator !== "++"
498
+ ) {
499
+ return false
500
+ }
501
+ } else {
502
+ return false
503
+ }
504
+
505
+ // Check body: must be a block statement
506
+ if (!j.BlockStatement.check(node.body)) {
507
+ return false
508
+ }
509
+
510
+ // Look for first statement that assigns arr[i] to a variable
511
+ if (node.body.body.length === 0) {
512
+ return false
513
+ }
514
+
515
+ const firstStmt = node.body.body[0]
516
+ if (!j.VariableDeclaration.check(firstStmt)) {
517
+ return false
518
+ }
519
+ if (firstStmt.declarations.length !== 1) {
520
+ return false
521
+ }
522
+ const varDeclarator = firstStmt.declarations[0]
523
+ if (!j.Identifier.check(varDeclarator.id)) {
524
+ return false
525
+ }
526
+ if (!j.MemberExpression.check(varDeclarator.init)) {
527
+ return false
528
+ }
529
+ if (
530
+ !j.Identifier.check(varDeclarator.init.object) ||
531
+ varDeclarator.init.object.name !== arrayVar
532
+ ) {
533
+ return false
534
+ }
535
+ if (
536
+ !j.Identifier.check(varDeclarator.init.property) ||
537
+ varDeclarator.init.property.name !== indexVar ||
538
+ varDeclarator.init.computed !== true
539
+ ) {
540
+ return false
541
+ }
542
+
543
+ // Check that the index variable is not used elsewhere in the body
544
+ const bodyWithoutFirst = node.body.body.slice(1)
545
+ let indexVarUsed = false
546
+
547
+ // Recursively check if identifier is used in AST nodes
548
+ const checkNode = (astNode) => {
549
+ if (!astNode || typeof astNode !== "object") return
550
+
551
+ if (astNode.type === "Identifier" && astNode.name === indexVar) {
552
+ indexVarUsed = true
553
+ return
554
+ }
555
+
556
+ // Traverse all properties
557
+ for (const key in astNode) {
558
+ if (
559
+ key === "loc" ||
560
+ key === "start" ||
561
+ key === "end" ||
562
+ key === "tokens" ||
563
+ key === "comments"
564
+ )
565
+ continue
566
+ const value = astNode[key]
567
+ if (Array.isArray(value)) {
568
+ value.forEach(checkNode)
569
+ } else if (value && typeof value === "object") {
570
+ checkNode(value)
571
+ }
572
+ }
573
+ }
574
+
575
+ bodyWithoutFirst.forEach(checkNode)
576
+
577
+ if (indexVarUsed) {
578
+ return false
579
+ }
580
+
581
+ return true
582
+ })
583
+ .forEach((path) => {
584
+ const node = path.node
585
+ const arrayVar = node.test.right.object.name
586
+ const itemVar = node.body.body[0].declarations[0].id.name
587
+ const itemKind = node.body.body[0].kind
588
+
589
+ // Create new body without the first declaration
590
+ const newBody = j.blockStatement(node.body.body.slice(1))
591
+
592
+ // Create for...of loop
593
+ const forOfLoop = j.forOfStatement(
594
+ j.variableDeclaration(itemKind, [j.variableDeclarator(j.identifier(itemVar))]),
595
+ j.identifier(arrayVar),
596
+ newBody,
597
+ )
598
+
599
+ j(path).replaceWith(forOfLoop)
600
+
601
+ modified = true
602
+ if (node.loc) {
603
+ changes.push({
604
+ type: "forLoopToForOf",
605
+ line: node.loc.start.line,
606
+ })
607
+ }
608
+ })
609
+
610
+ return { modified, changes }
611
+ }
612
+
613
+ /**
614
+ * Transform iterables' forEach() to for...of loop
615
+ * Handles DOM APIs like querySelectorAll, getElementsBy*, etc. and other known iterables
616
+ * Only transforms when forEach callback is declared inline with a function body (block statement)
617
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
618
+ */
619
+ export function iterableForEachToForOf(j, root) {
620
+ let modified = false
621
+ const changes = []
622
+
623
+ // Define known iterable-returning methods by their object/context
624
+ const knownIterableMethods = {
625
+ document: [
626
+ "querySelectorAll",
627
+ "getElementsByTagName",
628
+ "getElementsByClassName",
629
+ "getElementsByName",
630
+ ],
631
+ }
632
+
633
+ // Define known iterable properties
634
+ const knownIterableProperties = {
635
+ window: ["frames"],
636
+ }
637
+
638
+ root
639
+ .find(j.CallExpression)
640
+ .filter((path) => {
641
+ const node = path.node
642
+ // Check if this is a forEach call
643
+ if (
644
+ !j.MemberExpression.check(node.callee) ||
645
+ !j.Identifier.check(node.callee.property) ||
646
+ node.callee.property.name !== "forEach"
647
+ ) {
648
+ return false
649
+ }
650
+
651
+ const object = node.callee.object
652
+
653
+ // Check if this is a property access pattern like window.frames
654
+ if (j.MemberExpression.check(object) && !j.CallExpression.check(object)) {
655
+ const objectName = j.Identifier.check(object.object) ? object.object.name : null
656
+ const propertyName = j.Identifier.check(object.property)
657
+ ? object.property.name
658
+ : null
659
+
660
+ if (
661
+ objectName &&
662
+ propertyName &&
663
+ knownIterableProperties[objectName] &&
664
+ knownIterableProperties[objectName].includes(propertyName)
665
+ ) {
666
+ // This is a valid iterable property like window.frames - continue to callback check
667
+ } else {
668
+ // Not a known iterable property
669
+ return false
670
+ }
671
+ }
672
+ // Check for method call patterns like document.querySelectorAll()
673
+ else if (j.CallExpression.check(object)) {
674
+ // Check if it's a member expression (e.g., document.querySelectorAll)
675
+ if (!j.MemberExpression.check(object.callee)) {
676
+ return false
677
+ }
678
+
679
+ // Get the method name
680
+ const methodName = j.Identifier.check(object.callee.property)
681
+ ? object.callee.property.name
682
+ : null
683
+
684
+ if (!methodName) {
685
+ return false
686
+ }
687
+
688
+ // Verify the object is document only
689
+ const callerObject = object.callee.object
690
+ if (j.Identifier.check(callerObject)) {
691
+ const objectName = callerObject.name
692
+ // Only allow document
693
+ if (objectName !== "document") {
694
+ return false
695
+ }
696
+ // Verify method belongs to document
697
+ if (!knownIterableMethods.document.includes(methodName)) {
698
+ return false
699
+ }
700
+ }
701
+ // Handle cases like document.getElementById('x').querySelectorAll()
702
+ // Only allow these from document-originating chains
703
+ else if (
704
+ j.MemberExpression.check(callerObject) ||
705
+ j.CallExpression.check(callerObject)
706
+ ) {
707
+ // Check if this eventually chains from document
708
+ const isFromDocument = (node) => {
709
+ if (j.Identifier.check(node)) {
710
+ return node.name === "document"
711
+ }
712
+ if (j.MemberExpression.check(node)) {
713
+ return isFromDocument(node.object)
714
+ }
715
+ if (j.CallExpression.check(node)) {
716
+ if (j.MemberExpression.check(node.callee)) {
717
+ return isFromDocument(node.callee.object)
718
+ }
719
+ }
720
+ return false
721
+ }
722
+
723
+ if (!isFromDocument(callerObject)) {
724
+ return false
725
+ }
726
+
727
+ // Verify the method is valid for document
728
+ if (!knownIterableMethods.document.includes(methodName)) {
729
+ return false
730
+ }
731
+ } else {
732
+ return false
733
+ }
734
+ } else {
735
+ return false
736
+ }
737
+
738
+ // Check that forEach has a callback argument
739
+ if (node.arguments.length === 0) {
740
+ return false
741
+ }
742
+
743
+ const callback = node.arguments[0]
744
+ // Only transform if callback is an inline function (arrow or function expression)
745
+ if (
746
+ !j.ArrowFunctionExpression.check(callback) &&
747
+ !j.FunctionExpression.check(callback)
748
+ ) {
749
+ return false
750
+ }
751
+
752
+ // Only transform if the callback has a block statement body (with braces)
753
+ // Arrow functions with expression bodies (e.g., item => item.value) should NOT be transformed
754
+ if (!j.BlockStatement.check(callback.body)) {
755
+ return false
756
+ }
757
+
758
+ // Only transform if callback uses only the first parameter (element)
759
+ // Don't transform if it uses index or array parameters
760
+ const params = callback.params
761
+ if (params.length !== 1) {
762
+ return false
763
+ }
764
+
765
+ return true
766
+ })
767
+ .forEach((path) => {
768
+ const node = path.node
769
+ const iterable = node.callee.object
770
+ const callback = node.arguments[0]
771
+
772
+ const itemParam = callback.params[0]
773
+ const body = callback.body
774
+
775
+ // Create for...of loop
776
+ const forOfLoop = j.forOfStatement(
777
+ j.variableDeclaration("const", [j.variableDeclarator(itemParam)]),
778
+ iterable,
779
+ body,
780
+ )
781
+
782
+ // Replace the expression statement containing the forEach call
783
+ const statement = path.parent
784
+ if (j.ExpressionStatement.check(statement.node)) {
785
+ j(statement).replaceWith(forOfLoop)
786
+
787
+ modified = true
788
+ if (node.loc) {
789
+ changes.push({
790
+ type: "iterableForEachToForOf",
791
+ line: node.loc.start.line,
792
+ })
793
+ }
794
+ }
795
+ })
796
+
797
+ return { modified, changes }
798
+ }
package/tests/cli.test.js CHANGED
@@ -41,7 +41,7 @@ describe("CLI", () => {
41
41
 
42
42
  const transformedCode = fs.readFileSync(testFile, "utf8")
43
43
  assert.match(transformedCode, /const x = 1/)
44
- assert.match(result.stdout, /Summary: 1 file\(s\) upgraded/)
44
+ assert.match(result.stdout, /✓ 1 file\(s\) upgraded/)
45
45
  assert.strictEqual(result.status, 0)
46
46
  })
47
47
 
@@ -57,7 +57,6 @@ describe("CLI", () => {
57
57
  const transformedCode = fs.readFileSync(testFile, "utf8")
58
58
  assert.match(transformedCode, /const x = 1/)
59
59
  assert.doesNotMatch(transformedCode, /Promise\.try/) // Promise.try not in widely-available
60
- assert.match(result.stdout, /widely-available/)
61
60
  assert.strictEqual(result.status, 0)
62
61
  })
63
62
 
@@ -77,7 +76,6 @@ describe("CLI", () => {
77
76
  const transformedCode = fs.readFileSync(testFile, "utf8")
78
77
  assert.match(transformedCode, /const x = 1/)
79
78
  assert.match(transformedCode, /Promise\.try/) // Promise.try in newly-available
80
- assert.match(result.stdout, /newly-available/)
81
79
  assert.strictEqual(result.status, 0)
82
80
  })
83
81
 
@@ -108,7 +106,7 @@ describe("CLI", () => {
108
106
 
109
107
  const fileContent = fs.readFileSync(testFile, "utf8")
110
108
  assert.strictEqual(fileContent, originalCode) // File unchanged
111
- assert.match(result.stdout, /All files are already modern/)
109
+ assert.match(result.stdout, /All files are up to date/)
112
110
  assert.strictEqual(result.status, 0)
113
111
  })
114
112
 
@@ -128,8 +126,7 @@ describe("CLI", () => {
128
126
  const transformedCode = fs.readFileSync(testFile, "utf8")
129
127
  assert.match(transformedCode, /const x = 1/)
130
128
  assert.match(result.stdout, /✗/)
131
- assert.match(result.stdout, /Changes written to file/)
132
- assert.match(result.stdout, /1 file\(s\) upgraded/)
129
+ assert.match(result.stdout, /Changes have been written/)
133
130
  assert.strictEqual(result.status, 1) // Still exit 1 with --check
134
131
  })
135
132
 
@@ -150,7 +147,6 @@ describe("CLI", () => {
150
147
  const transformed2 = fs.readFileSync(file2, "utf8")
151
148
  assert.match(transformed1, /const x = 1/)
152
149
  assert.match(transformed2, /const y = 2/)
153
- assert.match(result.stdout, /Processing 2 file/)
154
150
  assert.match(result.stdout, /2 file\(s\) upgraded/)
155
151
  assert.strictEqual(result.status, 0)
156
152
  })
@@ -172,7 +168,7 @@ describe("CLI", () => {
172
168
  encoding: "utf8",
173
169
  })
174
170
 
175
- assert.match(result.stdout, /Processing 1 file/)
171
+ // Should only process file1 in tempDir, not in node_modules or .git
176
172
  assert.strictEqual(result.status, 0)
177
173
  })
178
174
 
@@ -191,7 +187,6 @@ describe("CLI", () => {
191
187
  encoding: "utf8",
192
188
  })
193
189
 
194
- assert.match(result.stdout, /Processing 4 file/)
195
190
  assert.match(result.stdout, /4 file\(s\) upgraded/)
196
191
  assert.strictEqual(result.status, 0)
197
192
  })
@@ -207,7 +202,6 @@ describe("CLI", () => {
207
202
  encoding: "utf8",
208
203
  })
209
204
 
210
- assert.match(result.stdout, /Processing 2 file/)
211
205
  assert.strictEqual(result.status, 0)
212
206
  })
213
207
 
@@ -250,7 +244,6 @@ describe("CLI", () => {
250
244
  })
251
245
 
252
246
  assert.match(result.stdout, /var to const/)
253
- assert.match(result.stdout, /line/)
254
247
  assert.strictEqual(result.status, 1)
255
248
  })
256
249
 
@@ -268,7 +261,7 @@ describe("CLI", () => {
268
261
  const transformed2 = fs.readFileSync(file2, "utf8")
269
262
  assert.match(transformed1, /const x = 1/)
270
263
  assert.match(transformed2, /const y = 2/)
271
- assert.match(result.stdout, /Processing 2 file/)
264
+ assert.match(result.stdout, /2 file\(s\) upgraded/)
272
265
  assert.strictEqual(result.status, 0)
273
266
  })
274
267
 
@@ -283,7 +276,7 @@ describe("CLI", () => {
283
276
 
284
277
  const fileContent = fs.readFileSync(testFile, "utf8")
285
278
  assert.strictEqual(fileContent, originalCode)
286
- assert.match(result.stdout, /All files are already modern/)
279
+ assert.match(result.stdout, /All files are up to date/)
287
280
  assert.strictEqual(result.status, 0)
288
281
  })
289
282
 
@@ -296,7 +289,7 @@ describe("CLI", () => {
296
289
  encoding: "utf8",
297
290
  })
298
291
 
299
- assert.match(result.stdout, /No changes:/)
292
+ assert.match(result.stdout, /All files are up to date/)
300
293
  assert.strictEqual(result.status, 0)
301
294
  })
302
295
 
@@ -322,7 +315,6 @@ describe("CLI", () => {
322
315
  })
323
316
 
324
317
  assert.match(result.stdout, /var to const/)
325
- assert.match(result.stdout, /lines:/)
326
318
  assert.strictEqual(result.status, 1)
327
319
  })
328
320
 
@@ -335,7 +327,7 @@ describe("CLI", () => {
335
327
  encoding: "utf8",
336
328
  })
337
329
 
338
- assert.match(result.stderr, /Error processing/)
330
+ assert.match(result.stderr, /✗ Error:/)
339
331
  assert.strictEqual(result.status, 0) // CLI continues despite errors
340
332
  })
341
333
 
@@ -352,7 +344,11 @@ describe("CLI", () => {
352
344
  encoding: "utf8",
353
345
  })
354
346
 
355
- assert.match(result.stdout, /Processing 2 file/)
347
+ const file1Content = fs.readFileSync(file1, "utf8")
348
+ const file2Content = fs.readFileSync(file2, "utf8")
349
+ assert.match(file1Content, /const x = 1/)
350
+ assert.match(file2Content, /const y = 2/)
351
+ assert.match(result.stdout, /2 file\(s\) upgraded/)
356
352
  assert.strictEqual(result.status, 0)
357
353
  })
358
354
  })