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.
@@ -480,6 +480,83 @@ var x = 1;`
480
480
  assert.strictEqual(result.modified, true)
481
481
  assert.match(result.code, /`/)
482
482
  })
483
+
484
+ test("numeric addition followed by string concatenation", () => {
485
+ const input = `cal_box.style.left = findPosX(cal_link) + 17 + 'px';`
486
+
487
+ const result = transform(input)
488
+
489
+ assert.strictEqual(result.modified, true)
490
+ // Should treat (findPosX(cal_link) + 17) as a single numeric expression
491
+ assert.match(result.code, /`\$\{findPosX\(cal_link\) \+ 17\}px`/)
492
+ })
493
+
494
+ test("multiple numeric additions followed by string concatenation", () => {
495
+ const input = `const result = a + b + c + 'd';`
496
+
497
+ const result = transform(input)
498
+
499
+ assert.strictEqual(result.modified, true)
500
+ // Should treat (a + b + c) as a single numeric expression
501
+ assert.match(result.code, /`\$\{a \+ b \+ c\}d`/)
502
+ })
503
+
504
+ test("string concatenation followed by numeric addition", () => {
505
+ const input = `const result = 'Value: ' + x + y;`
506
+
507
+ const result = transform(input)
508
+
509
+ assert.strictEqual(result.modified, true)
510
+ // After first string, all subsequent + are string concatenation
511
+ assert.match(result.code, /`Value: \$\{x\}\$\{y\}`/)
512
+ })
513
+
514
+ test("numeric addition in middle of string concatenations", () => {
515
+ const input = `const result = 'start' + (a + b) + 'end';`
516
+
517
+ const result = transform(input)
518
+
519
+ assert.strictEqual(result.modified, true)
520
+ // Parenthesized numeric expression should be preserved
521
+ // jscodeshift may add parentheses around binary expressions in template literals
522
+ assert.match(result.code, /`start\$\{(\()?a \+ b(\))?\}end`/)
523
+ })
524
+
525
+ test("consecutive string literals should be merged", () => {
526
+ const input = `const msg = 'Hello' + ' ' + 'world';`
527
+
528
+ const result = transform(input)
529
+
530
+ assert.strictEqual(result.modified, true)
531
+ assert.match(result.code, /`Hello world`/)
532
+ })
533
+
534
+ test("string literal followed by non-binary expression", () => {
535
+ const input = `const msg = 'Value: ' + getValue();`
536
+
537
+ const result = transform(input)
538
+
539
+ assert.strictEqual(result.modified, true)
540
+ assert.match(result.code, /`Value: \$\{getValue\(\)\}`/)
541
+ })
542
+
543
+ test("expression followed by string literal", () => {
544
+ const input = `const msg = getValue() + ' is the value';`
545
+
546
+ const result = transform(input)
547
+
548
+ assert.strictEqual(result.modified, true)
549
+ assert.match(result.code, /`\$\{getValue\(\)\} is the value`/)
550
+ })
551
+
552
+ test("non-binary expression in the middle", () => {
553
+ const input = `const msg = 'start' + getValue() + 'end';`
554
+
555
+ const result = transform(input)
556
+
557
+ assert.strictEqual(result.modified, true)
558
+ assert.match(result.code, /`start\$\{getValue\(\)\}end`/)
559
+ })
483
560
  })
484
561
 
485
562
  describe("object spread", () => {
@@ -521,6 +598,77 @@ var x = 1;`
521
598
  })
522
599
  })
523
600
 
601
+ describe("Math.pow() to exponentiation operator", () => {
602
+ test("Math.pow() to **", () => {
603
+ const input = `const result = Math.pow(2, 3);`
604
+
605
+ const result = transform(input)
606
+
607
+ assert.strictEqual(result.modified, true)
608
+ assert.match(result.code, /2 \*\* 3/)
609
+ })
610
+
611
+ test("Math.pow() with variables", () => {
612
+ const input = `const power = Math.pow(base, exponent);`
613
+
614
+ const result = transform(input)
615
+
616
+ assert.strictEqual(result.modified, true)
617
+ assert.match(result.code, /base \*\* exponent/)
618
+ })
619
+
620
+ test("Math.pow() with complex expressions", () => {
621
+ const input = `const result = Math.pow(x + 1, y * 2);`
622
+
623
+ const result = transform(input)
624
+
625
+ assert.strictEqual(result.modified, true)
626
+ assert.match(result.code, /\(x \+ 1\) \*\* \(y \* 2\)/)
627
+ })
628
+
629
+ test("Math.pow() in expressions", () => {
630
+ const input = `const area = Math.PI * Math.pow(radius, 2);`
631
+
632
+ const result = transform(input)
633
+
634
+ assert.strictEqual(result.modified, true)
635
+ assert.match(result.code, /Math\.PI \* radius \*\* 2/)
636
+ })
637
+
638
+ test("Math.pow() should not transform with wrong number of arguments", () => {
639
+ const input = `const result = Math.pow(2);`
640
+
641
+ const result = transform(input)
642
+
643
+ assert.strictEqual(result.modified, false)
644
+ assert.match(result.code, /Math\.pow\(2\)/)
645
+ })
646
+
647
+ test("Math.pow() nested calls", () => {
648
+ const input = `const result = Math.pow(Math.pow(2, 3), 4);`
649
+
650
+ const result = transform(input)
651
+
652
+ assert.strictEqual(result.modified, true)
653
+ // Only the outer Math.pow is transformed in a single pass
654
+ assert.match(result.code, /Math\.pow\(2, 3\) \*\* 4/)
655
+ })
656
+
657
+ test("Math.pow() tracks line numbers", () => {
658
+ const input = `// Line 1
659
+ const result = Math.pow(2, 3);`
660
+
661
+ const result = transform(input)
662
+
663
+ assert.strictEqual(result.modified, true)
664
+ const mathPowChanges = result.changes.filter(
665
+ (c) => c.type === "mathPowToExponentiation",
666
+ )
667
+ assert.strictEqual(mathPowChanges.length, 1)
668
+ assert.strictEqual(mathPowChanges[0].line, 2)
669
+ })
670
+ })
671
+
524
672
  test("no changes needed", () => {
525
673
  const input = `
526
674
  const x = 1;
@@ -562,4 +710,964 @@ var x = 1;`
562
710
  assert.strictEqual(result.modified, true)
563
711
  assert.match(result.code, /const x = 1/)
564
712
  })
713
+
714
+ describe("traditional for loop to for...of", () => {
715
+ test("basic for loop with array indexing", () => {
716
+ const input = `
717
+ for (let i = 0; i < items.length; i++) {
718
+ const item = items[i];
719
+ console.log(item);
720
+ }
721
+ `
722
+
723
+ const result = transform(input)
724
+
725
+ assert.strictEqual(result.modified, true)
726
+ assert.match(result.code, /for \(const item of items\)/)
727
+ assert.match(result.code, /console\.log\(item\)/)
728
+ assert.doesNotMatch(result.code, /items\[i\]/)
729
+ })
730
+
731
+ test("for loop with const variable", () => {
732
+ const input = `
733
+ for (let i = 0; i < arr.length; i++) {
734
+ const element = arr[i];
735
+ process(element);
736
+ }
737
+ `
738
+
739
+ const result = transform(input)
740
+
741
+ assert.strictEqual(result.modified, true)
742
+ assert.match(result.code, /for \(const element of arr\)/)
743
+ })
744
+
745
+ test("for loop with let variable", () => {
746
+ const input = `
747
+ for (let i = 0; i < arr.length; i++) {
748
+ let element = arr[i];
749
+ element = transform(element);
750
+ console.log(element);
751
+ }
752
+ `
753
+
754
+ const result = transform(input)
755
+
756
+ assert.strictEqual(result.modified, true)
757
+ assert.match(result.code, /for \(let element of arr\)/)
758
+ })
759
+
760
+ test("for loop with var variable", () => {
761
+ const input = `
762
+ for (let i = 0; i < arr.length; i++) {
763
+ var element = arr[i];
764
+ console.log(element);
765
+ }
766
+ `
767
+
768
+ const result = transform(input)
769
+
770
+ assert.strictEqual(result.modified, true)
771
+ // Note: var is also converted to const by the varToConst transformer
772
+ assert.match(result.code, /for \(const element of arr\)/)
773
+ })
774
+
775
+ test("should NOT transform when index is used in body", () => {
776
+ const input = `
777
+ for (let i = 0; i < items.length; i++) {
778
+ const item = items[i];
779
+ console.log(item, i);
780
+ }
781
+ `
782
+
783
+ const result = transform(input)
784
+
785
+ assert.strictEqual(result.modified, false)
786
+ assert.match(result.code, /for \(let i = 0; i < items\.length; i\+\+\)/)
787
+ })
788
+
789
+ test("should NOT transform when no array access statement", () => {
790
+ const input = `
791
+ for (let i = 0; i < items.length; i++) {
792
+ console.log(i);
793
+ }
794
+ `
795
+
796
+ const result = transform(input)
797
+
798
+ assert.strictEqual(result.modified, false)
799
+ assert.match(result.code, /for \(let i = 0/)
800
+ })
801
+
802
+ test("should NOT transform when body is empty", () => {
803
+ const input = `
804
+ for (let i = 0; i < items.length; i++) {
805
+ }
806
+ `
807
+
808
+ const result = transform(input)
809
+
810
+ assert.strictEqual(result.modified, false)
811
+ })
812
+
813
+ test("should NOT transform when using different increment", () => {
814
+ const input = `
815
+ for (let i = 0; i < items.length; i += 2) {
816
+ const item = items[i];
817
+ console.log(item);
818
+ }
819
+ `
820
+
821
+ const result = transform(input)
822
+
823
+ assert.strictEqual(result.modified, false)
824
+ assert.match(result.code, /i \+= 2/)
825
+ })
826
+
827
+ test("should NOT transform when starting from non-zero", () => {
828
+ const input = `
829
+ for (let i = 1; i < items.length; i++) {
830
+ const item = items[i];
831
+ console.log(item);
832
+ }
833
+ `
834
+
835
+ const result = transform(input)
836
+
837
+ assert.strictEqual(result.modified, false)
838
+ assert.match(result.code, /let i = 1/)
839
+ })
840
+
841
+ test("should NOT transform when using <= instead of <", () => {
842
+ const input = `
843
+ for (let i = 0; i <= items.length; i++) {
844
+ const item = items[i];
845
+ console.log(item);
846
+ }
847
+ `
848
+
849
+ const result = transform(input)
850
+
851
+ assert.strictEqual(result.modified, false)
852
+ assert.match(result.code, /i <= items\.length/)
853
+ })
854
+
855
+ test("should NOT transform when accessing different array", () => {
856
+ const input = `
857
+ for (let i = 0; i < items.length; i++) {
858
+ const item = otherArray[i];
859
+ console.log(item);
860
+ }
861
+ `
862
+
863
+ const result = transform(input)
864
+
865
+ assert.strictEqual(result.modified, false)
866
+ assert.match(result.code, /otherArray\[i\]/)
867
+ })
868
+
869
+ test("should NOT transform when first statement is not variable declaration", () => {
870
+ const input = `
871
+ for (let i = 0; i < items.length; i++) {
872
+ console.log(items[i]);
873
+ }
874
+ `
875
+
876
+ const result = transform(input)
877
+
878
+ assert.strictEqual(result.modified, false)
879
+ assert.match(result.code, /for \(let i = 0/)
880
+ })
881
+
882
+ test("should NOT transform when using different index variable", () => {
883
+ const input = `
884
+ for (let i = 0; i < items.length; i++) {
885
+ const item = items[j];
886
+ console.log(item);
887
+ }
888
+ `
889
+
890
+ const result = transform(input)
891
+
892
+ assert.strictEqual(result.modified, false)
893
+ })
894
+
895
+ test("should transform with ++i (prefix increment)", () => {
896
+ const input = `
897
+ for (let i = 0; i < items.length; ++i) {
898
+ const item = items[i];
899
+ console.log(item);
900
+ }
901
+ `
902
+
903
+ const result = transform(input)
904
+
905
+ assert.strictEqual(result.modified, true)
906
+ assert.match(result.code, /for \(const item of items\)/)
907
+ })
908
+
909
+ test("multiple statements after array access", () => {
910
+ const input = `
911
+ for (let i = 0; i < items.length; i++) {
912
+ const item = items[i];
913
+ console.log(item);
914
+ process(item);
915
+ cleanup();
916
+ }
917
+ `
918
+
919
+ const result = transform(input)
920
+
921
+ assert.strictEqual(result.modified, true)
922
+ assert.match(result.code, /for \(const item of items\)/)
923
+ assert.match(result.code, /console\.log\(item\)/)
924
+ assert.match(result.code, /process\(item\)/)
925
+ assert.match(result.code, /cleanup\(\)/)
926
+ })
927
+
928
+ test("should NOT transform when init is not a variable declaration", () => {
929
+ const input = `
930
+ for (i = 0; i < items.length; i++) {
931
+ const item = items[i];
932
+ console.log(item);
933
+ }
934
+ `
935
+
936
+ const result = transform(input)
937
+
938
+ assert.strictEqual(result.modified, false)
939
+ })
940
+
941
+ test("should NOT transform when init has multiple declarations", () => {
942
+ const input = `
943
+ for (let i = 0, j = 0; i < items.length; i++) {
944
+ const item = items[i];
945
+ console.log(item);
946
+ }
947
+ `
948
+
949
+ const result = transform(input)
950
+
951
+ assert.strictEqual(result.modified, false)
952
+ })
953
+
954
+ test("should NOT transform when init id is not an identifier", () => {
955
+ const input = `
956
+ for (let [i] = [0]; i < items.length; i++) {
957
+ const item = items[i];
958
+ console.log(item);
959
+ }
960
+ `
961
+
962
+ const result = transform(input)
963
+
964
+ assert.strictEqual(result.modified, false)
965
+ })
966
+
967
+ test("should NOT transform when test is not a binary expression", () => {
968
+ const input = `
969
+ for (let i = 0; items.length; i++) {
970
+ const item = items[i];
971
+ console.log(item);
972
+ }
973
+ `
974
+
975
+ const result = transform(input)
976
+
977
+ assert.strictEqual(result.modified, false)
978
+ })
979
+
980
+ test("should NOT transform when test operator is not <", () => {
981
+ const input = `
982
+ for (let i = 0; i <= items.length; i++) {
983
+ const item = items[i];
984
+ console.log(item);
985
+ }
986
+ `
987
+
988
+ const result = transform(input)
989
+
990
+ assert.strictEqual(result.modified, false)
991
+ })
992
+
993
+ test("should NOT transform when test left is not the index variable", () => {
994
+ const input = `
995
+ for (let i = 0; j < items.length; i++) {
996
+ const item = items[i];
997
+ console.log(item);
998
+ }
999
+ `
1000
+
1001
+ const result = transform(input)
1002
+
1003
+ assert.strictEqual(result.modified, false)
1004
+ })
1005
+
1006
+ test("should NOT transform when test right is not a member expression", () => {
1007
+ const input = `
1008
+ for (let i = 0; i < 10; i++) {
1009
+ const item = items[i];
1010
+ console.log(item);
1011
+ }
1012
+ `
1013
+
1014
+ const result = transform(input)
1015
+
1016
+ assert.strictEqual(result.modified, false)
1017
+ })
1018
+
1019
+ test("should NOT transform when test right property is not 'length'", () => {
1020
+ const input = `
1021
+ for (let i = 0; i < items.size; i++) {
1022
+ const item = items[i];
1023
+ console.log(item);
1024
+ }
1025
+ `
1026
+
1027
+ const result = transform(input)
1028
+
1029
+ assert.strictEqual(result.modified, false)
1030
+ })
1031
+
1032
+ test("should NOT transform when test right object is not an identifier", () => {
1033
+ const input = `
1034
+ for (let i = 0; i < getItems().length; i++) {
1035
+ const item = getItems()[i];
1036
+ console.log(item);
1037
+ }
1038
+ `
1039
+
1040
+ const result = transform(input)
1041
+
1042
+ assert.strictEqual(result.modified, false)
1043
+ })
1044
+
1045
+ test("should NOT transform when update is not an update expression", () => {
1046
+ const input = `
1047
+ for (let i = 0; i < items.length; i = i + 1) {
1048
+ const item = items[i];
1049
+ console.log(item);
1050
+ }
1051
+ `
1052
+
1053
+ const result = transform(input)
1054
+
1055
+ assert.strictEqual(result.modified, false)
1056
+ })
1057
+
1058
+ test("should NOT transform when update argument is not the index variable", () => {
1059
+ const input = `
1060
+ for (let i = 0; i < items.length; j++) {
1061
+ const item = items[i];
1062
+ console.log(item);
1063
+ }
1064
+ `
1065
+
1066
+ const result = transform(input)
1067
+
1068
+ assert.strictEqual(result.modified, false)
1069
+ })
1070
+
1071
+ test("should NOT transform when update operator is not ++", () => {
1072
+ const input = `
1073
+ for (let i = 0; i < items.length; i--) {
1074
+ const item = items[i];
1075
+ console.log(item);
1076
+ }
1077
+ `
1078
+
1079
+ const result = transform(input)
1080
+
1081
+ assert.strictEqual(result.modified, false)
1082
+ })
1083
+
1084
+ test("should NOT transform when body is not a block statement", () => {
1085
+ const input = `
1086
+ for (let i = 0; i < items.length; i++)
1087
+ console.log(items[i]);
1088
+ `
1089
+
1090
+ const result = transform(input)
1091
+
1092
+ assert.strictEqual(result.modified, false)
1093
+ })
1094
+
1095
+ test("should NOT transform when first statement has multiple declarations", () => {
1096
+ const input = `
1097
+ for (let i = 0; i < items.length; i++) {
1098
+ const item = items[i], other = null;
1099
+ console.log(item);
1100
+ }
1101
+ `
1102
+
1103
+ const result = transform(input)
1104
+
1105
+ assert.strictEqual(result.modified, false)
1106
+ })
1107
+
1108
+ test("should NOT transform when first statement id is not an identifier", () => {
1109
+ const input = `
1110
+ for (let i = 0; i < items.length; i++) {
1111
+ const [item] = items[i];
1112
+ console.log(item);
1113
+ }
1114
+ `
1115
+
1116
+ const result = transform(input)
1117
+
1118
+ assert.strictEqual(result.modified, false)
1119
+ })
1120
+
1121
+ test("should NOT transform when first statement init is not a member expression", () => {
1122
+ const input = `
1123
+ for (let i = 0; i < items.length; i++) {
1124
+ const item = getItem(i);
1125
+ console.log(item);
1126
+ }
1127
+ `
1128
+
1129
+ const result = transform(input)
1130
+
1131
+ assert.strictEqual(result.modified, false)
1132
+ })
1133
+
1134
+ test("should NOT transform when member expression object name doesn't match", () => {
1135
+ const input = `
1136
+ for (let i = 0; i < items.length; i++) {
1137
+ const item = other[i];
1138
+ console.log(item);
1139
+ }
1140
+ `
1141
+
1142
+ const result = transform(input)
1143
+
1144
+ assert.strictEqual(result.modified, false)
1145
+ })
1146
+
1147
+ test("should NOT transform when member expression property doesn't match index", () => {
1148
+ const input = `
1149
+ for (let i = 0; i < items.length; i++) {
1150
+ const item = items[j];
1151
+ console.log(item);
1152
+ }
1153
+ `
1154
+
1155
+ const result = transform(input)
1156
+
1157
+ assert.strictEqual(result.modified, false)
1158
+ })
1159
+
1160
+ test("should NOT transform when member expression is not computed", () => {
1161
+ const input = `
1162
+ for (let i = 0; i < items.length; i++) {
1163
+ const item = items.i;
1164
+ console.log(item);
1165
+ }
1166
+ `
1167
+
1168
+ const result = transform(input)
1169
+
1170
+ assert.strictEqual(result.modified, false)
1171
+ })
1172
+
1173
+ test("tracks line numbers for forLoopToForOf", () => {
1174
+ const input = `// Line 1
1175
+ for (let i = 0; i < items.length; i++) {
1176
+ const item = items[i];
1177
+ console.log(item);
1178
+ }`
1179
+
1180
+ const result = transform(input)
1181
+
1182
+ assert.strictEqual(result.modified, true)
1183
+ const forLoopChanges = result.changes.filter((c) => c.type === "forLoopToForOf")
1184
+ assert.strictEqual(forLoopChanges.length, 1)
1185
+ assert.strictEqual(forLoopChanges[0].line, 2)
1186
+ })
1187
+ })
1188
+
1189
+ describe("iterable forEach to for...of", () => {
1190
+ test("document.querySelectorAll().forEach() to for...of", () => {
1191
+ const input = `
1192
+ document.querySelectorAll('.item').forEach(item => {
1193
+ console.log(item);
1194
+ });
1195
+ `
1196
+
1197
+ const result = transform(input)
1198
+
1199
+ assert.strictEqual(result.modified, true)
1200
+ assert.match(
1201
+ result.code,
1202
+ /for \(const item of document\.querySelectorAll\(['"]\.item['"]\)\)/,
1203
+ )
1204
+ assert.match(result.code, /console\.log\(item\)/)
1205
+ })
1206
+
1207
+ test("document.getElementsByTagName().forEach() to for...of", () => {
1208
+ const input = `
1209
+ document.getElementsByTagName('div').forEach(div => {
1210
+ div.classList.add('active');
1211
+ });
1212
+ `
1213
+
1214
+ const result = transform(input)
1215
+
1216
+ assert.strictEqual(result.modified, true)
1217
+ assert.match(
1218
+ result.code,
1219
+ /for \(const div of document\.getElementsByTagName\(['"]div['"]\)\)/,
1220
+ )
1221
+ })
1222
+
1223
+ test("document.getElementsByClassName().forEach() to for...of", () => {
1224
+ const input = `
1225
+ document.getElementsByClassName('button').forEach(button => {
1226
+ button.disabled = true;
1227
+ });
1228
+ `
1229
+
1230
+ const result = transform(input)
1231
+
1232
+ assert.strictEqual(result.modified, true)
1233
+ assert.match(
1234
+ result.code,
1235
+ /for \(const button of document\.getElementsByClassName\(['"]button['"]\)\)/,
1236
+ )
1237
+ })
1238
+
1239
+ test("document.getElementsByName().forEach() to for...of", () => {
1240
+ const input = `
1241
+ document.getElementsByName('email').forEach(input => {
1242
+ input.required = true;
1243
+ });
1244
+ `
1245
+
1246
+ const result = transform(input)
1247
+
1248
+ assert.strictEqual(result.modified, true)
1249
+ assert.match(
1250
+ result.code,
1251
+ /for \(const input of document\.getElementsByName\(['"]email['"]\)\)/,
1252
+ )
1253
+ })
1254
+
1255
+ test("element variable querySelectorAll should NOT transform", () => {
1256
+ const input = `
1257
+ element.querySelectorAll('span').forEach(span => {
1258
+ span.remove();
1259
+ });
1260
+ `
1261
+
1262
+ const result = transform(input)
1263
+
1264
+ // Should NOT transform because element is not from document chain
1265
+ assert.strictEqual(result.modified, false)
1266
+ assert.match(result.code, /element\.querySelectorAll/)
1267
+ })
1268
+
1269
+ test("chained element method .querySelectorAll().forEach() to for...of", () => {
1270
+ const input = `
1271
+ document.getElementById('container').querySelectorAll('p').forEach(p => {
1272
+ p.textContent = '';
1273
+ });
1274
+ `
1275
+
1276
+ const result = transform(input)
1277
+
1278
+ assert.strictEqual(result.modified, true)
1279
+ assert.match(
1280
+ result.code,
1281
+ /for \(const p of document\.getElementById\(['"]container['"]\)\.querySelectorAll\(['"]p['"]\)\)/,
1282
+ )
1283
+ })
1284
+
1285
+ test("window.frames property forEach() to for...of", () => {
1286
+ const input = `
1287
+ window.frames.forEach(frame => {
1288
+ frame.postMessage('hello', '*');
1289
+ });
1290
+ `
1291
+
1292
+ const result = transform(input)
1293
+
1294
+ assert.strictEqual(result.modified, true)
1295
+ assert.match(result.code, /for \(const frame of window\.frames\)/)
1296
+ })
1297
+
1298
+ test("should NOT transform arrow function without braces (expression body)", () => {
1299
+ const input = `
1300
+ document.querySelectorAll('.item').forEach(item => item.remove());
1301
+ `
1302
+
1303
+ const result = transform(input)
1304
+
1305
+ assert.strictEqual(result.modified, false)
1306
+ assert.match(result.code, /forEach\(item => item\.remove\(\)\)/)
1307
+ })
1308
+
1309
+ test("should NOT transform with index parameter", () => {
1310
+ const input = `
1311
+ document.querySelectorAll('.item').forEach((item, index) => {
1312
+ console.log(item, index);
1313
+ });
1314
+ `
1315
+
1316
+ const result = transform(input)
1317
+
1318
+ assert.strictEqual(result.modified, false)
1319
+ assert.match(result.code, /forEach\(\(item, index\) =>/)
1320
+ })
1321
+
1322
+ test("should NOT transform with array parameter", () => {
1323
+ const input = `
1324
+ document.querySelectorAll('.item').forEach((item, index, array) => {
1325
+ console.log(item, index, array);
1326
+ });
1327
+ `
1328
+
1329
+ const result = transform(input)
1330
+
1331
+ assert.strictEqual(result.modified, false)
1332
+ assert.match(result.code, /forEach\(\(item, index, array\) =>/)
1333
+ })
1334
+
1335
+ test("should NOT transform with non-inline callback (reference)", () => {
1336
+ const input = `
1337
+ document.querySelectorAll('.item').forEach(handleItem);
1338
+ `
1339
+
1340
+ const result = transform(input)
1341
+
1342
+ assert.strictEqual(result.modified, false)
1343
+ assert.match(result.code, /forEach\(handleItem\)/)
1344
+ })
1345
+
1346
+ test("should NOT transform without callback", () => {
1347
+ const input = `
1348
+ document.querySelectorAll('.item').forEach();
1349
+ `
1350
+
1351
+ const result = transform(input)
1352
+
1353
+ assert.strictEqual(result.modified, false)
1354
+ assert.match(result.code, /forEach\(\)/)
1355
+ })
1356
+
1357
+ test("should NOT transform unknown methods", () => {
1358
+ const input = `
1359
+ document.querySomething('.item').forEach(item => {
1360
+ console.log(item);
1361
+ });
1362
+ `
1363
+
1364
+ const result = transform(input)
1365
+
1366
+ assert.strictEqual(result.modified, false)
1367
+ assert.match(result.code, /querySomething/)
1368
+ })
1369
+
1370
+ test("should NOT transform non-document objects with querySelectorAll", () => {
1371
+ const input = `
1372
+ myObject.querySelectorAll('.item').forEach(item => {
1373
+ console.log(item);
1374
+ });
1375
+ `
1376
+
1377
+ const result = transform(input)
1378
+
1379
+ assert.strictEqual(result.modified, false)
1380
+ assert.match(result.code, /myObject\.querySelectorAll/)
1381
+ })
1382
+
1383
+ test("should NOT transform window methods not in allowed list", () => {
1384
+ const input = `
1385
+ window.querySelectorAll('.item').forEach(item => {
1386
+ console.log(item);
1387
+ });
1388
+ `
1389
+
1390
+ const result = transform(input)
1391
+
1392
+ assert.strictEqual(result.modified, false)
1393
+ assert.match(result.code, /window\.querySelectorAll/)
1394
+ })
1395
+
1396
+ test("transforms with function expression", () => {
1397
+ const input = `
1398
+ document.querySelectorAll('button').forEach(function(btn) {
1399
+ btn.disabled = true;
1400
+ });
1401
+ `
1402
+
1403
+ const result = transform(input)
1404
+
1405
+ assert.strictEqual(result.modified, true)
1406
+ assert.match(
1407
+ result.code,
1408
+ /for \(const btn of document\.querySelectorAll\(['"]button['"]\)\)/,
1409
+ )
1410
+ })
1411
+
1412
+ test("tracks line numbers correctly", () => {
1413
+ const input = `// Line 1
1414
+ document.querySelectorAll('.item').forEach(item => {
1415
+ console.log(item);
1416
+ });`
1417
+
1418
+ const result = transform(input)
1419
+
1420
+ assert.strictEqual(result.modified, true)
1421
+ assert.strictEqual(result.changes.length, 1)
1422
+ assert.strictEqual(result.changes[0].type, "iterableForEachToForOf")
1423
+ assert.strictEqual(result.changes[0].line, 2)
1424
+ })
1425
+
1426
+ test("handles complex selector strings", () => {
1427
+ const input = `
1428
+ document.querySelectorAll('[data-toggle="modal"]').forEach(el => {
1429
+ el.addEventListener('click', handleClick);
1430
+ });
1431
+ `
1432
+
1433
+ const result = transform(input)
1434
+
1435
+ assert.strictEqual(result.modified, true)
1436
+ assert.match(
1437
+ result.code,
1438
+ /for \(const el of document\.querySelectorAll\(['"]\[data-toggle="modal"\]['"]\)\)/,
1439
+ )
1440
+ })
1441
+
1442
+ test("preserves multiline function bodies", () => {
1443
+ const input = `
1444
+ document.querySelectorAll('.item').forEach(item => {
1445
+ const value = item.value;
1446
+ console.log(value);
1447
+ item.classList.add('processed');
1448
+ });
1449
+ `
1450
+
1451
+ const result = transform(input)
1452
+
1453
+ assert.strictEqual(result.modified, true)
1454
+ assert.match(result.code, /for \(const item of/)
1455
+ assert.match(result.code, /const value = item\.value/)
1456
+ assert.match(result.code, /console\.log\(value\)/)
1457
+ assert.match(result.code, /item\.classList\.add/)
1458
+ })
1459
+
1460
+ test("should NOT transform element variables with getElementsByTagName", () => {
1461
+ const input = `
1462
+ container.getElementsByTagName('input').forEach(input => {
1463
+ input.value = '';
1464
+ });
1465
+ `
1466
+
1467
+ const result = transform(input)
1468
+
1469
+ // Should not transform because container is not from document chain
1470
+ assert.strictEqual(result.modified, false)
1471
+ assert.match(result.code, /container\.getElementsByTagName/)
1472
+ })
1473
+
1474
+ test("should NOT transform element variables with getElementsByClassName", () => {
1475
+ const input = `
1476
+ section.getElementsByClassName('warning').forEach(warning => {
1477
+ warning.style.display = 'none';
1478
+ });
1479
+ `
1480
+
1481
+ const result = transform(input)
1482
+
1483
+ // Should not transform because section is not from document chain
1484
+ assert.strictEqual(result.modified, false)
1485
+ assert.match(result.code, /section\.getElementsByClassName/)
1486
+ })
1487
+
1488
+ test("should NOT transform window.querySelectorAll (not in allowed methods)", () => {
1489
+ const input = `
1490
+ window.querySelectorAll('.item').forEach(item => {
1491
+ console.log(item);
1492
+ });
1493
+ `
1494
+
1495
+ const result = transform(input)
1496
+
1497
+ // Should not transform because querySelectorAll is not a window method
1498
+ assert.strictEqual(result.modified, false)
1499
+ assert.match(result.code, /window\.querySelectorAll/)
1500
+ })
1501
+
1502
+ test("should NOT transform property access on unknown objects", () => {
1503
+ const input = `
1504
+ customObject.frames.forEach(frame => {
1505
+ frame.postMessage('test', '*');
1506
+ });
1507
+ `
1508
+
1509
+ const result = transform(input)
1510
+
1511
+ // Should not transform because customObject is not window
1512
+ assert.strictEqual(result.modified, false)
1513
+ assert.match(result.code, /customObject\.frames/)
1514
+ })
1515
+
1516
+ test("should NOT transform when method callee is not a member expression", () => {
1517
+ const input = `
1518
+ getSomething().forEach(item => {
1519
+ console.log(item);
1520
+ });
1521
+ `
1522
+
1523
+ const result = transform(input)
1524
+
1525
+ // Should not transform because callee is not a member expression
1526
+ assert.strictEqual(result.modified, false)
1527
+ assert.match(result.code, /getSomething\(\)\.forEach/)
1528
+ })
1529
+
1530
+ test("should NOT transform when method name cannot be extracted (computed property)", () => {
1531
+ const input = `
1532
+ document['querySelectorAll']('.item').forEach(item => {
1533
+ console.log(item);
1534
+ });
1535
+ `
1536
+
1537
+ const result = transform(input)
1538
+
1539
+ // Should not transform because method name is computed (string literal, not identifier)
1540
+ assert.strictEqual(result.modified, false)
1541
+ assert.match(result.code, /document\['querySelectorAll'\]/)
1542
+ })
1543
+
1544
+ test("should NOT transform unknown document methods", () => {
1545
+ const input = `
1546
+ document.customMethod().forEach(item => {
1547
+ console.log(item);
1548
+ });
1549
+ `
1550
+
1551
+ const result = transform(input)
1552
+
1553
+ // Should not transform because customMethod is not in allowed list
1554
+ assert.strictEqual(result.modified, false)
1555
+ assert.match(result.code, /document\.customMethod/)
1556
+ })
1557
+
1558
+ test("should NOT transform chained call from non-document origin", () => {
1559
+ const input = `
1560
+ element.querySelector('div').querySelectorAll('span').forEach(span => {
1561
+ span.remove();
1562
+ });
1563
+ `
1564
+
1565
+ const result = transform(input)
1566
+
1567
+ // Should not transform because chain doesn't start with document
1568
+ assert.strictEqual(result.modified, false)
1569
+ assert.match(result.code, /element\.querySelector/)
1570
+ })
1571
+
1572
+ test("should NOT transform chained call with unknown method", () => {
1573
+ const input = `
1574
+ document.getElementById('x').customMethod().forEach(item => {
1575
+ console.log(item);
1576
+ });
1577
+ `
1578
+
1579
+ const result = transform(input)
1580
+
1581
+ // Should not transform because customMethod is not in document methods
1582
+ assert.strictEqual(result.modified, false)
1583
+ assert.match(result.code, /customMethod/)
1584
+ })
1585
+
1586
+ test("should NOT transform when caller object is neither identifier nor member/call expression", () => {
1587
+ const input = `
1588
+ (function() { return document; })().querySelectorAll('.item').forEach(item => {
1589
+ console.log(item);
1590
+ });
1591
+ `
1592
+
1593
+ const result = transform(input)
1594
+
1595
+ // Should not transform because caller is a function expression
1596
+ assert.strictEqual(result.modified, false)
1597
+ assert.match(result.code, /forEach/)
1598
+ })
1599
+
1600
+ test("should NOT transform when caller object is ThisExpression", () => {
1601
+ const input = `
1602
+ this.querySelectorAll('.item').forEach(item => {
1603
+ console.log(item);
1604
+ });
1605
+ `
1606
+
1607
+ const result = transform(input)
1608
+
1609
+ // Should not transform because caller is this, not document
1610
+ assert.strictEqual(result.modified, false)
1611
+ assert.match(result.code, /this\.querySelectorAll/)
1612
+ })
1613
+
1614
+ test("should NOT transform when forEach object is neither MemberExpression nor CallExpression", () => {
1615
+ const input = `
1616
+ items.forEach(item => {
1617
+ console.log(item);
1618
+ });
1619
+ `
1620
+
1621
+ const result = transform(input)
1622
+
1623
+ // Should not transform because items is just an identifier, not a method call or property
1624
+ assert.strictEqual(result.modified, false)
1625
+ assert.match(result.code, /items\.forEach/)
1626
+ })
1627
+
1628
+ test("deeply nested document chain should transform", () => {
1629
+ const input = `
1630
+ document.getElementById('a').querySelector('b').querySelectorAll('c').forEach(item => {
1631
+ item.remove();
1632
+ });
1633
+ `
1634
+
1635
+ const result = transform(input)
1636
+
1637
+ // Should transform because it chains from document
1638
+ assert.strictEqual(result.modified, true)
1639
+ assert.match(
1640
+ result.code,
1641
+ /for \(const item of document\.getElementById\(['"]a['"]\)\.querySelector\(['"]b['"]\)\.querySelectorAll\(['"]c['"]\)\)/,
1642
+ )
1643
+ })
1644
+
1645
+ test("should NOT transform when callee in chain is not a member expression", () => {
1646
+ const input = `
1647
+ getDocument().querySelectorAll('span').forEach(item => {
1648
+ item.textContent = 'test';
1649
+ });
1650
+ `
1651
+
1652
+ const result = transform(input)
1653
+
1654
+ // Should NOT transform - chain starts with function call, not document
1655
+ assert.strictEqual(result.modified, false)
1656
+ assert.match(result.code, /getDocument\(\)\.querySelectorAll/)
1657
+ })
1658
+
1659
+ test("should transform document property access with querySelectorAll", () => {
1660
+ const input = `
1661
+ document.body.querySelectorAll('div').forEach(div => {
1662
+ div.remove();
1663
+ });
1664
+ `
1665
+
1666
+ const result = transform(input)
1667
+
1668
+ // Should transform - chains from document through property access
1669
+ assert.strictEqual(result.modified, true)
1670
+ assert.match(result.code, /for \(const div of document\.body\.querySelectorAll/)
1671
+ })
1672
+ })
565
1673
  })