esupgrade 2025.2.1 → 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 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) guidlines.
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
- #### `.concat()` → [Array spread [...]][mdn-spread]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.2.1",
3
+ "version": "2025.3.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -796,3 +796,363 @@ export function iterableForEachToForOf(j, root) {
796
796
 
797
797
  return { modified, changes }
798
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(function(n) { console.log(n); });`
46
+ const input = `numbers.forEach((n) => { console.log(n); });`
47
47
 
48
48
  const result = transform(input)
49
49
 
@@ -1585,7 +1585,7 @@ document.querySelectorAll('.item').forEach(item => {
1585
1585
 
1586
1586
  test("should NOT transform when caller object is neither identifier nor member/call expression", () => {
1587
1587
  const input = `
1588
- (function() { return document; })().querySelectorAll('.item').forEach(item => {
1588
+ (() => { return document; })().querySelectorAll('.item').forEach(item => {
1589
1589
  console.log(item);
1590
1590
  });
1591
1591
  `
@@ -1670,4 +1670,420 @@ document.querySelectorAll('.item').forEach(item => {
1670
1670
  assert.match(result.code, /for \(const div of document\.body\.querySelectorAll/)
1671
1671
  })
1672
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
+ })
1673
2089
  })