esupgrade 2025.2.0 → 2025.3.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 -1
- package/README.md +13 -1
- package/package.json +1 -1
- package/src/widelyAvailable.js +433 -16
- package/tests/widelyAvailable.test.js +495 -2
package/AGENTS.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
When writing code, you MUST ALWAYS follow the [naming-things](https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md)
|
|
1
|
+
When writing code, you MUST ALWAYS follow the [naming-things](https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md) guidelines.
|
|
2
2
|
|
|
3
3
|
All code must be fully tested with a 100% coverage. Unreachable code must be removed.
|
|
4
|
+
|
|
5
|
+
All transformers must be documented in the README.md.
|
package/README.md
CHANGED
|
@@ -142,6 +142,17 @@ Supports:
|
|
|
142
142
|
> Transformations limited to inline arrow or function expressions with block statement bodies.
|
|
143
143
|
> Callbacks with index parameters or expression bodies are not transformed.
|
|
144
144
|
|
|
145
|
+
#### `for...of Object.keys()` → [`for...in` loops][mdn-for-in]
|
|
146
|
+
|
|
147
|
+
```diff
|
|
148
|
+
-for (const key of Object.keys(obj)) {
|
|
149
|
+
- console.log(key);
|
|
150
|
+
-}
|
|
151
|
+
+for (const key in obj) {
|
|
152
|
+
+ console.log(key);
|
|
153
|
+
+}
|
|
154
|
+
```
|
|
155
|
+
|
|
145
156
|
#### `Array.from()` → [Array spread [...]][mdn-spread]
|
|
146
157
|
|
|
147
158
|
```diff
|
|
@@ -165,7 +176,7 @@ Supports:
|
|
|
165
176
|
+const copy = { ...original };
|
|
166
177
|
```
|
|
167
178
|
|
|
168
|
-
####
|
|
179
|
+
#### `Array.concat()` → [Array spread [...]][mdn-spread]
|
|
169
180
|
|
|
170
181
|
```diff
|
|
171
182
|
-const combined = arr1.concat(arr2, arr3);
|
|
@@ -222,6 +233,7 @@ Supports:
|
|
|
222
233
|
[mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
|
223
234
|
[mdn-const]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
|
|
224
235
|
[mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
|
|
236
|
+
[mdn-for-in]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
|
|
225
237
|
[mdn-for-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
|
|
226
238
|
[mdn-let]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
|
|
227
239
|
[mdn-promise-try]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try
|
package/package.json
CHANGED
package/src/widelyAvailable.js
CHANGED
|
@@ -51,26 +51,83 @@ export function concatToTemplateLiteral(j, root) {
|
|
|
51
51
|
const parts = []
|
|
52
52
|
const expressions = []
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
|
@@ -739,3 +796,363 @@ export function iterableForEachToForOf(j, root) {
|
|
|
739
796
|
|
|
740
797
|
return { modified, changes }
|
|
741
798
|
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Transform anonymous function expressions to arrow functions
|
|
802
|
+
* Does not transform if the function:
|
|
803
|
+
* - is a named function expression (useful for stack traces and recursion)
|
|
804
|
+
* - uses 'this' (arrow functions don't have their own 'this')
|
|
805
|
+
* - uses 'arguments' (arrow functions don't have 'arguments' object)
|
|
806
|
+
* - uses 'super' (defensive check, though this would be a syntax error in function expressions)
|
|
807
|
+
* - is a generator function (arrow functions cannot be generators)
|
|
808
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
|
809
|
+
*/
|
|
810
|
+
export function anonymousFunctionToArrow(j, root) {
|
|
811
|
+
let modified = false
|
|
812
|
+
const changes = []
|
|
813
|
+
|
|
814
|
+
// Helper to check if a node or its descendants use 'this'
|
|
815
|
+
const usesThis = (node) => {
|
|
816
|
+
let found = false
|
|
817
|
+
|
|
818
|
+
const checkNode = (astNode) => {
|
|
819
|
+
if (!astNode || typeof astNode !== "object" || found) return
|
|
820
|
+
|
|
821
|
+
// Found 'this' identifier
|
|
822
|
+
if (astNode.type === "ThisExpression") {
|
|
823
|
+
found = true
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Don't traverse into nested function declarations or expressions
|
|
828
|
+
// as they have their own 'this' context
|
|
829
|
+
if (
|
|
830
|
+
astNode.type === "FunctionDeclaration" ||
|
|
831
|
+
astNode.type === "FunctionExpression" ||
|
|
832
|
+
astNode.type === "ArrowFunctionExpression"
|
|
833
|
+
) {
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Traverse all properties
|
|
838
|
+
for (const key in astNode) {
|
|
839
|
+
if (
|
|
840
|
+
key === "loc" ||
|
|
841
|
+
key === "start" ||
|
|
842
|
+
key === "end" ||
|
|
843
|
+
key === "tokens" ||
|
|
844
|
+
key === "comments"
|
|
845
|
+
)
|
|
846
|
+
continue
|
|
847
|
+
const value = astNode[key]
|
|
848
|
+
if (Array.isArray(value)) {
|
|
849
|
+
value.forEach(checkNode)
|
|
850
|
+
} else if (value && typeof value === "object") {
|
|
851
|
+
checkNode(value)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
checkNode(node)
|
|
857
|
+
return found
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Helper to check if a node or its descendants use 'arguments'
|
|
861
|
+
const usesArguments = (node) => {
|
|
862
|
+
let found = false
|
|
863
|
+
|
|
864
|
+
const visit = (n) => {
|
|
865
|
+
if (!n || typeof n !== "object" || found) return
|
|
866
|
+
|
|
867
|
+
// If we encounter a nested function, don't traverse into it
|
|
868
|
+
// as it has its own 'arguments' binding
|
|
869
|
+
if (n.type === "FunctionExpression" || n.type === "FunctionDeclaration") {
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Check if this is an 'arguments' identifier
|
|
874
|
+
if (n.type === "Identifier" && n.name === "arguments") {
|
|
875
|
+
found = true
|
|
876
|
+
return
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Traverse all child nodes
|
|
880
|
+
for (const key in n) {
|
|
881
|
+
if (
|
|
882
|
+
key === "loc" ||
|
|
883
|
+
key === "start" ||
|
|
884
|
+
key === "end" ||
|
|
885
|
+
key === "tokens" ||
|
|
886
|
+
key === "comments" ||
|
|
887
|
+
key === "type"
|
|
888
|
+
) {
|
|
889
|
+
continue
|
|
890
|
+
}
|
|
891
|
+
const value = n[key]
|
|
892
|
+
if (Array.isArray(value)) {
|
|
893
|
+
for (const item of value) {
|
|
894
|
+
visit(item)
|
|
895
|
+
if (found) return
|
|
896
|
+
}
|
|
897
|
+
} else if (value && typeof value === "object") {
|
|
898
|
+
visit(value)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Start visiting from the function body's child nodes
|
|
904
|
+
// Don't check the body node itself, check its contents
|
|
905
|
+
// Note: FunctionExpression.body is always a BlockStatement
|
|
906
|
+
if (node.type === "BlockStatement" && node.body) {
|
|
907
|
+
for (const statement of node.body) {
|
|
908
|
+
visit(statement)
|
|
909
|
+
if (found) break
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return found
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Helper to check if a node or its descendants use 'super'
|
|
917
|
+
const usesSuper = (node) => {
|
|
918
|
+
let found = false
|
|
919
|
+
|
|
920
|
+
const visit = (n) => {
|
|
921
|
+
if (!n || typeof n !== "object" || found) return
|
|
922
|
+
|
|
923
|
+
// If we encounter a nested function, don't traverse into it
|
|
924
|
+
// as it has its own 'super' binding context
|
|
925
|
+
if (n.type === "FunctionExpression" || n.type === "FunctionDeclaration") {
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Check if this is a 'super' node
|
|
930
|
+
if (n.type === "Super") {
|
|
931
|
+
found = true
|
|
932
|
+
return
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Traverse all child nodes
|
|
936
|
+
for (const key in n) {
|
|
937
|
+
if (
|
|
938
|
+
key === "loc" ||
|
|
939
|
+
key === "start" ||
|
|
940
|
+
key === "end" ||
|
|
941
|
+
key === "tokens" ||
|
|
942
|
+
key === "comments" ||
|
|
943
|
+
key === "type"
|
|
944
|
+
) {
|
|
945
|
+
continue
|
|
946
|
+
}
|
|
947
|
+
const value = n[key]
|
|
948
|
+
if (Array.isArray(value)) {
|
|
949
|
+
for (const item of value) {
|
|
950
|
+
visit(item)
|
|
951
|
+
if (found) return
|
|
952
|
+
}
|
|
953
|
+
} else if (value && typeof value === "object") {
|
|
954
|
+
visit(value)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Start visiting from the function body's child nodes
|
|
960
|
+
// Don't check the body node itself, check its contents
|
|
961
|
+
// Note: FunctionExpression.body is always a BlockStatement
|
|
962
|
+
if (node.type === "BlockStatement" && node.body) {
|
|
963
|
+
for (const statement of node.body) {
|
|
964
|
+
visit(statement)
|
|
965
|
+
if (found) break
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return found
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
root
|
|
973
|
+
.find(j.FunctionExpression)
|
|
974
|
+
.filter((path) => {
|
|
975
|
+
const node = path.node
|
|
976
|
+
|
|
977
|
+
// Skip if it's a named function expression
|
|
978
|
+
// Named functions are useful for stack traces and recursion
|
|
979
|
+
if (node.id) {
|
|
980
|
+
return false
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Skip if it's a generator function
|
|
984
|
+
if (node.generator) {
|
|
985
|
+
return false
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Skip if it uses 'this'
|
|
989
|
+
if (usesThis(node.body)) {
|
|
990
|
+
return false
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Skip if it uses 'arguments'
|
|
994
|
+
const hasArguments = usesArguments(node.body)
|
|
995
|
+
if (hasArguments) {
|
|
996
|
+
return false
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Skip if it uses 'super'
|
|
1000
|
+
if (usesSuper(node.body)) {
|
|
1001
|
+
return false
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return true
|
|
1005
|
+
})
|
|
1006
|
+
.forEach((path) => {
|
|
1007
|
+
const node = path.node
|
|
1008
|
+
|
|
1009
|
+
// Create arrow function with same params and body
|
|
1010
|
+
const arrowFunction = j.arrowFunctionExpression(node.params, node.body, false)
|
|
1011
|
+
|
|
1012
|
+
// Preserve async property
|
|
1013
|
+
if (node.async) {
|
|
1014
|
+
arrowFunction.async = true
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
j(path).replaceWith(arrowFunction)
|
|
1018
|
+
|
|
1019
|
+
modified = true
|
|
1020
|
+
if (node.loc) {
|
|
1021
|
+
changes.push({
|
|
1022
|
+
type: "anonymousFunctionToArrow",
|
|
1023
|
+
line: node.loc.start.line,
|
|
1024
|
+
})
|
|
1025
|
+
}
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
return { modified, changes }
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Transform Array.concat() to array spread syntax
|
|
1033
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
|
|
1034
|
+
*/
|
|
1035
|
+
export function arrayConcatToSpread(j, root) {
|
|
1036
|
+
let modified = false
|
|
1037
|
+
const changes = []
|
|
1038
|
+
|
|
1039
|
+
// Helper to check if an expression is statically verifiable as an array
|
|
1040
|
+
const isVerifiableArray = (node) => {
|
|
1041
|
+
// Array literal: [1, 2, 3]
|
|
1042
|
+
if (j.ArrayExpression.check(node)) {
|
|
1043
|
+
return true
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Array.from(), Array.of(), etc.
|
|
1047
|
+
if (
|
|
1048
|
+
j.CallExpression.check(node) &&
|
|
1049
|
+
j.MemberExpression.check(node.callee) &&
|
|
1050
|
+
j.Identifier.check(node.callee.object) &&
|
|
1051
|
+
node.callee.object.name === "Array"
|
|
1052
|
+
) {
|
|
1053
|
+
return true
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// new Array()
|
|
1057
|
+
if (
|
|
1058
|
+
j.NewExpression.check(node) &&
|
|
1059
|
+
j.Identifier.check(node.callee) &&
|
|
1060
|
+
node.callee.name === "Array"
|
|
1061
|
+
) {
|
|
1062
|
+
return true
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const STRING_METHODS_RETURNING_ARRAY = [
|
|
1066
|
+
"matchAll",
|
|
1067
|
+
"split",
|
|
1068
|
+
"slice",
|
|
1069
|
+
"substr",
|
|
1070
|
+
"substring",
|
|
1071
|
+
"toLowerCase",
|
|
1072
|
+
"toUpperCase",
|
|
1073
|
+
"trim",
|
|
1074
|
+
"trimStart",
|
|
1075
|
+
"trimEnd",
|
|
1076
|
+
]
|
|
1077
|
+
|
|
1078
|
+
// .split() on strings returns arrays
|
|
1079
|
+
if (
|
|
1080
|
+
j.CallExpression.check(node) &&
|
|
1081
|
+
j.MemberExpression.check(node.callee) &&
|
|
1082
|
+
j.Identifier.check(node.callee.property) &&
|
|
1083
|
+
j.StringLiteral.check(node.callee.object) &&
|
|
1084
|
+
STRING_METHODS_RETURNING_ARRAY.includes(node.callee.property.name)
|
|
1085
|
+
) {
|
|
1086
|
+
return true
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return false
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
root
|
|
1093
|
+
.find(j.CallExpression)
|
|
1094
|
+
.filter((path) => {
|
|
1095
|
+
const node = path.node
|
|
1096
|
+
|
|
1097
|
+
// Check if this is a .concat() call
|
|
1098
|
+
if (
|
|
1099
|
+
!j.MemberExpression.check(node.callee) ||
|
|
1100
|
+
!j.Identifier.check(node.callee.property) ||
|
|
1101
|
+
node.callee.property.name !== "concat"
|
|
1102
|
+
) {
|
|
1103
|
+
return false
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Must have at least one argument
|
|
1107
|
+
if (node.arguments.length === 0) {
|
|
1108
|
+
return false
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Only transform if we can verify the object is an array
|
|
1112
|
+
const object = node.callee.object
|
|
1113
|
+
if (!isVerifiableArray(object)) {
|
|
1114
|
+
return false
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return true
|
|
1118
|
+
})
|
|
1119
|
+
.forEach((path) => {
|
|
1120
|
+
const node = path.node
|
|
1121
|
+
const baseArray = node.callee.object
|
|
1122
|
+
const concatArgs = node.arguments
|
|
1123
|
+
|
|
1124
|
+
// Build array elements: start with spread of base array
|
|
1125
|
+
const elements = [j.spreadElement(baseArray)]
|
|
1126
|
+
|
|
1127
|
+
// Add each concat argument
|
|
1128
|
+
concatArgs.forEach((arg) => {
|
|
1129
|
+
// If the argument is an array literal, spread it
|
|
1130
|
+
// Otherwise, check if it's likely an array (could be iterable)
|
|
1131
|
+
if (j.ArrayExpression.check(arg)) {
|
|
1132
|
+
// Spread array literals
|
|
1133
|
+
elements.push(j.spreadElement(arg))
|
|
1134
|
+
} else {
|
|
1135
|
+
// For non-array arguments, we need to determine if they should be spread
|
|
1136
|
+
// In concat(), arrays are flattened one level, primitives are added as-is
|
|
1137
|
+
// Since we can't statically determine types, we spread everything
|
|
1138
|
+
// This matches concat's behavior for arrays and iterables
|
|
1139
|
+
elements.push(j.spreadElement(arg))
|
|
1140
|
+
}
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
// Create new array expression with spread elements
|
|
1144
|
+
const spreadArray = j.arrayExpression(elements)
|
|
1145
|
+
|
|
1146
|
+
j(path).replaceWith(spreadArray)
|
|
1147
|
+
|
|
1148
|
+
modified = true
|
|
1149
|
+
if (node.loc) {
|
|
1150
|
+
changes.push({
|
|
1151
|
+
type: "arrayConcatToSpread",
|
|
1152
|
+
line: node.loc.start.line,
|
|
1153
|
+
})
|
|
1154
|
+
}
|
|
1155
|
+
})
|
|
1156
|
+
|
|
1157
|
+
return { modified, changes }
|
|
1158
|
+
}
|
|
@@ -43,7 +43,7 @@ describe("widely-available", () => {
|
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
test("forEach should NOT transform plain identifiers with function expression", () => {
|
|
46
|
-
const input = `numbers.forEach(
|
|
46
|
+
const input = `numbers.forEach((n) => { console.log(n); });`
|
|
47
47
|
|
|
48
48
|
const result = transform(input)
|
|
49
49
|
|
|
@@ -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", () => {
|
|
@@ -1508,7 +1585,7 @@ document.querySelectorAll('.item').forEach(item => {
|
|
|
1508
1585
|
|
|
1509
1586
|
test("should NOT transform when caller object is neither identifier nor member/call expression", () => {
|
|
1510
1587
|
const input = `
|
|
1511
|
-
(
|
|
1588
|
+
(() => { return document; })().querySelectorAll('.item').forEach(item => {
|
|
1512
1589
|
console.log(item);
|
|
1513
1590
|
});
|
|
1514
1591
|
`
|
|
@@ -1593,4 +1670,420 @@ document.querySelectorAll('.item').forEach(item => {
|
|
|
1593
1670
|
assert.match(result.code, /for \(const div of document\.body\.querySelectorAll/)
|
|
1594
1671
|
})
|
|
1595
1672
|
})
|
|
1673
|
+
|
|
1674
|
+
describe("arrow functions", () => {
|
|
1675
|
+
test("should transform simple anonymous function to arrow function", () => {
|
|
1676
|
+
const input = `
|
|
1677
|
+
const greet = function(name) {
|
|
1678
|
+
return "Hello " + name;
|
|
1679
|
+
};
|
|
1680
|
+
`
|
|
1681
|
+
|
|
1682
|
+
const result = transform(input)
|
|
1683
|
+
|
|
1684
|
+
assert.strictEqual(result.modified, true)
|
|
1685
|
+
// Single parameter arrow functions don't need parentheses
|
|
1686
|
+
assert.match(result.code, /const greet = name =>/)
|
|
1687
|
+
assert.match(result.code, /return/)
|
|
1688
|
+
})
|
|
1689
|
+
|
|
1690
|
+
test("should transform anonymous function with multiple parameters", () => {
|
|
1691
|
+
const input = `
|
|
1692
|
+
const add = function(a, b) {
|
|
1693
|
+
return a + b;
|
|
1694
|
+
};
|
|
1695
|
+
`
|
|
1696
|
+
|
|
1697
|
+
const result = transform(input)
|
|
1698
|
+
|
|
1699
|
+
assert.strictEqual(result.modified, true)
|
|
1700
|
+
assert.match(result.code, /const add = \(a, b\) =>/)
|
|
1701
|
+
})
|
|
1702
|
+
|
|
1703
|
+
test("should transform anonymous function with no parameters", () => {
|
|
1704
|
+
const input = `
|
|
1705
|
+
const getValue = function() {
|
|
1706
|
+
return 42;
|
|
1707
|
+
};
|
|
1708
|
+
`
|
|
1709
|
+
|
|
1710
|
+
const result = transform(input)
|
|
1711
|
+
|
|
1712
|
+
assert.strictEqual(result.modified, true)
|
|
1713
|
+
assert.match(result.code, /const getValue = \(\) =>/)
|
|
1714
|
+
})
|
|
1715
|
+
|
|
1716
|
+
test("should transform callback function", () => {
|
|
1717
|
+
const input = `[1, 2, 3].map(function(x) { return x * 2; });`
|
|
1718
|
+
|
|
1719
|
+
const result = transform(input)
|
|
1720
|
+
|
|
1721
|
+
assert.strictEqual(result.modified, true)
|
|
1722
|
+
// Single parameter doesn't need parentheses
|
|
1723
|
+
assert.match(result.code, /\[1, 2, 3\]\.map\(x =>/)
|
|
1724
|
+
})
|
|
1725
|
+
|
|
1726
|
+
test("should NOT transform function using 'this'", () => {
|
|
1727
|
+
const input = `
|
|
1728
|
+
const obj = {
|
|
1729
|
+
method: function() {
|
|
1730
|
+
return this.value;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
`
|
|
1734
|
+
|
|
1735
|
+
const result = transform(input)
|
|
1736
|
+
|
|
1737
|
+
assert.strictEqual(result.modified, false)
|
|
1738
|
+
assert.match(result.code, /method: function\(\)/)
|
|
1739
|
+
})
|
|
1740
|
+
|
|
1741
|
+
test("should NOT transform function using 'this' in nested code", () => {
|
|
1742
|
+
const input = `
|
|
1743
|
+
const handler = function() {
|
|
1744
|
+
if (true) {
|
|
1745
|
+
console.log(this.name);
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
`
|
|
1749
|
+
|
|
1750
|
+
const result = transform(input)
|
|
1751
|
+
|
|
1752
|
+
assert.strictEqual(result.modified, false)
|
|
1753
|
+
assert.match(result.code, /const handler = function\(\)/)
|
|
1754
|
+
})
|
|
1755
|
+
|
|
1756
|
+
test("should NOT transform function using 'arguments'", () => {
|
|
1757
|
+
const input = `
|
|
1758
|
+
const sum = function() {
|
|
1759
|
+
return Array.from(arguments).reduce((a, b) => a + b, 0);
|
|
1760
|
+
};
|
|
1761
|
+
`
|
|
1762
|
+
|
|
1763
|
+
const result = transform(input)
|
|
1764
|
+
|
|
1765
|
+
// The function itself should NOT be transformed to an arrow function
|
|
1766
|
+
// (other transformations like Array.from -> spread may still happen)
|
|
1767
|
+
assert.match(result.code, /const sum = function\(\)/)
|
|
1768
|
+
// Ensure it's not an arrow function
|
|
1769
|
+
assert.doesNotMatch(result.code, /const sum = \(\) =>/)
|
|
1770
|
+
assert.doesNotMatch(result.code, /const sum = =>/)
|
|
1771
|
+
})
|
|
1772
|
+
|
|
1773
|
+
test("should NOT transform generator function", () => {
|
|
1774
|
+
const input = `
|
|
1775
|
+
const gen = function*() {
|
|
1776
|
+
yield 1;
|
|
1777
|
+
yield 2;
|
|
1778
|
+
};
|
|
1779
|
+
`
|
|
1780
|
+
|
|
1781
|
+
const result = transform(input)
|
|
1782
|
+
|
|
1783
|
+
assert.strictEqual(result.modified, false)
|
|
1784
|
+
assert.match(result.code, /const gen = function\*\(\)/)
|
|
1785
|
+
})
|
|
1786
|
+
|
|
1787
|
+
test("should transform nested function that doesn't use 'this'", () => {
|
|
1788
|
+
const input = `
|
|
1789
|
+
const outer = function(x) {
|
|
1790
|
+
return function(y) {
|
|
1791
|
+
return x + y;
|
|
1792
|
+
};
|
|
1793
|
+
};
|
|
1794
|
+
`
|
|
1795
|
+
|
|
1796
|
+
const result = transform(input)
|
|
1797
|
+
|
|
1798
|
+
// Both functions should be transformed
|
|
1799
|
+
assert.strictEqual(result.modified, true)
|
|
1800
|
+
// Single parameters don't need parentheses
|
|
1801
|
+
assert.match(result.code, /const outer = x =>/)
|
|
1802
|
+
// The inner function should also be transformed
|
|
1803
|
+
assert.match(result.code, /return y =>/)
|
|
1804
|
+
})
|
|
1805
|
+
|
|
1806
|
+
test("should NOT transform outer function but transform inner when outer uses 'this'", () => {
|
|
1807
|
+
const input = `
|
|
1808
|
+
const outer = function() {
|
|
1809
|
+
this.value = 10;
|
|
1810
|
+
return function(x) {
|
|
1811
|
+
return x * 2;
|
|
1812
|
+
};
|
|
1813
|
+
};
|
|
1814
|
+
`
|
|
1815
|
+
|
|
1816
|
+
const result = transform(input)
|
|
1817
|
+
|
|
1818
|
+
// Only inner function should be transformed
|
|
1819
|
+
assert.strictEqual(result.modified, true)
|
|
1820
|
+
assert.match(result.code, /const outer = function\(\)/)
|
|
1821
|
+
// Single parameter doesn't need parentheses
|
|
1822
|
+
assert.match(result.code, /return x =>/)
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
test("should transform async function", () => {
|
|
1826
|
+
const input = `
|
|
1827
|
+
const fetchData = async function(url) {
|
|
1828
|
+
const response = await fetch(url);
|
|
1829
|
+
return response.json();
|
|
1830
|
+
};
|
|
1831
|
+
`
|
|
1832
|
+
|
|
1833
|
+
const result = transform(input)
|
|
1834
|
+
|
|
1835
|
+
assert.strictEqual(result.modified, true)
|
|
1836
|
+
// Single parameter doesn't need parentheses
|
|
1837
|
+
assert.match(result.code, /const fetchData = async url =>/)
|
|
1838
|
+
})
|
|
1839
|
+
|
|
1840
|
+
test("should transform function with complex body", () => {
|
|
1841
|
+
const input = `
|
|
1842
|
+
const process = function(data) {
|
|
1843
|
+
const result = [];
|
|
1844
|
+
for (const item of data) {
|
|
1845
|
+
result.push(item * 2);
|
|
1846
|
+
}
|
|
1847
|
+
return result;
|
|
1848
|
+
};
|
|
1849
|
+
`
|
|
1850
|
+
|
|
1851
|
+
const result = transform(input)
|
|
1852
|
+
|
|
1853
|
+
assert.strictEqual(result.modified, true)
|
|
1854
|
+
// Single parameter doesn't need parentheses
|
|
1855
|
+
assert.match(result.code, /const process = data =>/)
|
|
1856
|
+
})
|
|
1857
|
+
|
|
1858
|
+
test("should handle multiple transformations in same code", () => {
|
|
1859
|
+
const input = `
|
|
1860
|
+
const fn1 = function(x) { return x + 1; };
|
|
1861
|
+
const fn2 = function(y) { return y * 2; };
|
|
1862
|
+
`
|
|
1863
|
+
|
|
1864
|
+
const result = transform(input)
|
|
1865
|
+
|
|
1866
|
+
assert.strictEqual(result.modified, true)
|
|
1867
|
+
// Single parameters don't need parentheses
|
|
1868
|
+
assert.match(result.code, /const fn1 = x =>/)
|
|
1869
|
+
assert.match(result.code, /const fn2 = y =>/)
|
|
1870
|
+
})
|
|
1871
|
+
|
|
1872
|
+
test("should NOT transform when 'this' is in nested function scope", () => {
|
|
1873
|
+
const input = `
|
|
1874
|
+
const outer = function(x) {
|
|
1875
|
+
return function() {
|
|
1876
|
+
return this.value + x;
|
|
1877
|
+
};
|
|
1878
|
+
};
|
|
1879
|
+
`
|
|
1880
|
+
|
|
1881
|
+
const result = transform(input)
|
|
1882
|
+
|
|
1883
|
+
// Outer should transform, inner should not
|
|
1884
|
+
assert.strictEqual(result.modified, true)
|
|
1885
|
+
// Single parameter doesn't need parentheses
|
|
1886
|
+
assert.match(result.code, /const outer = x =>/)
|
|
1887
|
+
assert.match(result.code, /return function\(\)/)
|
|
1888
|
+
})
|
|
1889
|
+
|
|
1890
|
+
test("should transform event handlers without 'this'", () => {
|
|
1891
|
+
const input = `
|
|
1892
|
+
button.addEventListener('click', function(event) {
|
|
1893
|
+
console.log('Clicked', event.target);
|
|
1894
|
+
});
|
|
1895
|
+
`
|
|
1896
|
+
|
|
1897
|
+
const result = transform(input)
|
|
1898
|
+
|
|
1899
|
+
assert.strictEqual(result.modified, true)
|
|
1900
|
+
// Single parameter doesn't need parentheses
|
|
1901
|
+
assert.match(result.code, /button\.addEventListener\('click', event =>/)
|
|
1902
|
+
})
|
|
1903
|
+
|
|
1904
|
+
test("should transform IIFE without 'this'", () => {
|
|
1905
|
+
const input = `
|
|
1906
|
+
(function() {
|
|
1907
|
+
console.log('IIFE executed');
|
|
1908
|
+
})();
|
|
1909
|
+
`
|
|
1910
|
+
|
|
1911
|
+
const result = transform(input)
|
|
1912
|
+
|
|
1913
|
+
assert.strictEqual(result.modified, true)
|
|
1914
|
+
assert.match(result.code, /\(\(\) =>/)
|
|
1915
|
+
})
|
|
1916
|
+
|
|
1917
|
+
test("should NOT transform named function expression", () => {
|
|
1918
|
+
// Named function expressions should be kept as-is for stack traces and recursion
|
|
1919
|
+
const input = `
|
|
1920
|
+
const factorial = function fact(n) {
|
|
1921
|
+
return n <= 1 ? 1 : n * fact(n - 1);
|
|
1922
|
+
};
|
|
1923
|
+
`
|
|
1924
|
+
|
|
1925
|
+
const result = transform(input)
|
|
1926
|
+
|
|
1927
|
+
// Named function expressions should not be transformed
|
|
1928
|
+
assert.strictEqual(result.modified, false)
|
|
1929
|
+
assert.match(result.code, /function fact\(n\)/)
|
|
1930
|
+
})
|
|
1931
|
+
})
|
|
1932
|
+
|
|
1933
|
+
describe("Array.concat() to spread", () => {
|
|
1934
|
+
test("[].concat(other) to [...[], ...other]", () => {
|
|
1935
|
+
const input = `const result = [1, 2].concat(other);`
|
|
1936
|
+
|
|
1937
|
+
const result = transform(input)
|
|
1938
|
+
|
|
1939
|
+
assert.strictEqual(result.modified, true)
|
|
1940
|
+
assert.match(result.code, /\[\.\..\[1, 2\], \.\.\.other\]/)
|
|
1941
|
+
})
|
|
1942
|
+
|
|
1943
|
+
test("[].concat([1, 2, 3]) with array literal", () => {
|
|
1944
|
+
const input = `const result = [].concat([1, 2, 3]);`
|
|
1945
|
+
|
|
1946
|
+
const result = transform(input)
|
|
1947
|
+
|
|
1948
|
+
assert.strictEqual(result.modified, true)
|
|
1949
|
+
assert.match(result.code, /\[\.\..\[\], \.\.\.\[1, 2, 3\]\]/)
|
|
1950
|
+
})
|
|
1951
|
+
|
|
1952
|
+
test("[].concat(item1, item2, item3) with multiple arguments", () => {
|
|
1953
|
+
const input = `const result = [].concat(other1, other2, other3);`
|
|
1954
|
+
|
|
1955
|
+
const result = transform(input)
|
|
1956
|
+
|
|
1957
|
+
assert.strictEqual(result.modified, true)
|
|
1958
|
+
assert.match(
|
|
1959
|
+
result.code,
|
|
1960
|
+
/\[\.\..\[\], \.\.\.other1, \.\.\.other2, \.\.\.other3\]/,
|
|
1961
|
+
)
|
|
1962
|
+
})
|
|
1963
|
+
|
|
1964
|
+
test("concat in expression", () => {
|
|
1965
|
+
const input = `const length = [].concat(other).length;`
|
|
1966
|
+
|
|
1967
|
+
const result = transform(input)
|
|
1968
|
+
|
|
1969
|
+
assert.strictEqual(result.modified, true)
|
|
1970
|
+
assert.match(result.code, /\[\.\..\[\], \.\.\.other\]\.length/)
|
|
1971
|
+
})
|
|
1972
|
+
|
|
1973
|
+
test("concat with method call result", () => {
|
|
1974
|
+
const input = `const result = [].concat(getItems());`
|
|
1975
|
+
|
|
1976
|
+
const result = transform(input)
|
|
1977
|
+
|
|
1978
|
+
assert.strictEqual(result.modified, true)
|
|
1979
|
+
assert.match(result.code, /\[\.\..\[\], \.\.\.getItems\(\)\]/)
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
test("should NOT transform concat with no arguments", () => {
|
|
1983
|
+
const input = `const copy = arr.concat();`
|
|
1984
|
+
|
|
1985
|
+
const result = transform(input)
|
|
1986
|
+
|
|
1987
|
+
// concat() with no args is just a shallow copy, but we don't transform it
|
|
1988
|
+
assert.strictEqual(result.modified, false)
|
|
1989
|
+
assert.match(result.code, /arr\.concat\(\)/)
|
|
1990
|
+
})
|
|
1991
|
+
|
|
1992
|
+
test("concat tracks line numbers", () => {
|
|
1993
|
+
const input = `// Line 1
|
|
1994
|
+
const result = [1, 2].concat(other);`
|
|
1995
|
+
|
|
1996
|
+
const result = transform(input)
|
|
1997
|
+
|
|
1998
|
+
assert.strictEqual(result.modified, true)
|
|
1999
|
+
assert.strictEqual(result.changes.length, 1)
|
|
2000
|
+
assert.strictEqual(result.changes[0].type, "arrayConcatToSpread")
|
|
2001
|
+
assert.strictEqual(result.changes[0].line, 2)
|
|
2002
|
+
})
|
|
2003
|
+
|
|
2004
|
+
test("concat in arrow function", () => {
|
|
2005
|
+
const input = `const fn = (arr, other) => [1, 2].concat(other);`
|
|
2006
|
+
|
|
2007
|
+
const result = transform(input)
|
|
2008
|
+
|
|
2009
|
+
assert.strictEqual(result.modified, true)
|
|
2010
|
+
assert.match(result.code, /\[\.\..\[1, 2\], \.\.\.other\]/)
|
|
2011
|
+
})
|
|
2012
|
+
|
|
2013
|
+
test("nested array with concat", () => {
|
|
2014
|
+
const input = `const result = [[1, 2]].concat([[3, 4]]);`
|
|
2015
|
+
|
|
2016
|
+
const result = transform(input)
|
|
2017
|
+
|
|
2018
|
+
assert.strictEqual(result.modified, true)
|
|
2019
|
+
assert.match(result.code, /\[\.\..\[\[1, 2\]\], \.\.\.\[\[3, 4\]\]\]/)
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
test("should NOT transform string.concat()", () => {
|
|
2023
|
+
const input = `const result = str.concat("hello");`
|
|
2024
|
+
|
|
2025
|
+
const result = transform(input)
|
|
2026
|
+
|
|
2027
|
+
// Should not transform - str is not verifiably an array
|
|
2028
|
+
assert.strictEqual(result.modified, false)
|
|
2029
|
+
assert.match(result.code, /str\.concat/)
|
|
2030
|
+
})
|
|
2031
|
+
|
|
2032
|
+
test("should NOT transform concat on unknown identifier", () => {
|
|
2033
|
+
const input = `const result = arr.concat(other);`
|
|
2034
|
+
|
|
2035
|
+
const result = transform(input)
|
|
2036
|
+
|
|
2037
|
+
// Should not transform - arr is just an identifier, not verifiably an array
|
|
2038
|
+
assert.strictEqual(result.modified, false)
|
|
2039
|
+
assert.match(result.code, /arr\.concat/)
|
|
2040
|
+
})
|
|
2041
|
+
|
|
2042
|
+
test("should transform concat on array literal", () => {
|
|
2043
|
+
const input = `const result = [1, 2, 3].concat([4, 5, 6]);`
|
|
2044
|
+
|
|
2045
|
+
const result = transform(input)
|
|
2046
|
+
|
|
2047
|
+
assert.strictEqual(result.modified, true)
|
|
2048
|
+
assert.match(result.code, /\[\.\..\[1, 2, 3\], \.\.\.\[4, 5, 6\]\]/)
|
|
2049
|
+
})
|
|
2050
|
+
|
|
2051
|
+
test("should transform concat on Array.from()", () => {
|
|
2052
|
+
const input = `const result = Array.from(items).concat(more);`
|
|
2053
|
+
|
|
2054
|
+
const result = transform(input)
|
|
2055
|
+
|
|
2056
|
+
assert.strictEqual(result.modified, true)
|
|
2057
|
+
// Both arrayFromToSpread and arrayConcatToSpread run
|
|
2058
|
+
// Array.from(items) -> [...items], then [...items].concat(more) -> [...[...items], ...more]
|
|
2059
|
+
assert.match(result.code, /\[\.\..\[\.\.\.items\], \.\.\.more\]/)
|
|
2060
|
+
})
|
|
2061
|
+
|
|
2062
|
+
test("should transform concat on String.slice() result", () => {
|
|
2063
|
+
const input = `const result = "lorem ipsum".slice(0, 10).concat(more);`
|
|
2064
|
+
|
|
2065
|
+
const result = transform(input)
|
|
2066
|
+
|
|
2067
|
+
assert.strictEqual(result.modified, true)
|
|
2068
|
+
assert.match(result.code, /\[\.\.\."lorem ipsum"\.slice\(0, 10\), \.\.\.more\]/)
|
|
2069
|
+
})
|
|
2070
|
+
|
|
2071
|
+
test("should transform concat on String.split() result", () => {
|
|
2072
|
+
const input = `const result = "foo,bar".split(',').concat(more);`
|
|
2073
|
+
|
|
2074
|
+
const result = transform(input)
|
|
2075
|
+
|
|
2076
|
+
assert.strictEqual(result.modified, true)
|
|
2077
|
+
assert.match(result.code, /\[\.\.\."foo,bar"\.split\(','\), \.\.\.more\]/)
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
test("should transform concat on new Array()", () => {
|
|
2081
|
+
const input = `const result = new Array(5).concat(more);`
|
|
2082
|
+
|
|
2083
|
+
const result = transform(input)
|
|
2084
|
+
|
|
2085
|
+
assert.strictEqual(result.modified, true)
|
|
2086
|
+
assert.match(result.code, /\[\.\.\.new Array\(5\), \.\.\.more\]/)
|
|
2087
|
+
})
|
|
2088
|
+
})
|
|
1596
2089
|
})
|