esupgrade 2025.20.1 → 2025.21.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.
@@ -28,7 +28,7 @@ jobs:
28
28
  node-version-file: package.json
29
29
  - run: npm ci
30
30
  - run: node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=lcov.info
31
- - uses: codecov/codecov-action@v6
31
+ - uses: codecov/codecov-action@v7
32
32
  with:
33
33
  token: ${{ secrets.CODECOV_TOKEN }}
34
34
  flags: javascript
package/README.md CHANGED
@@ -10,6 +10,10 @@
10
10
 
11
11
  Keeping your JavaScript and TypeScript code up to date with full browser compatibility.
12
12
 
13
+ ## Sponsors
14
+
15
+ [![Sponsors](https://django.the-box.sh/sponsors/codingjoe/esupgrade.svg)](https://github.com/sponsors/codingjoe)
16
+
13
17
  ## Usage
14
18
 
15
19
  esupgrade is safe and meant to be used automatically on your codebase.
@@ -229,12 +233,21 @@ Supports:
229
233
  ```diff
230
234
  -const budget = 1000000;
231
235
  -const maxUsers = 1000000n;
236
+ -const permission = 0o1234567;
237
+ -const mask = 0xabcdef;
238
+ -const flags = 0b1010000111000011;
239
+ -const ratio = 100000.25;
240
+ -const large = 1e100000;
232
241
  +const budget = 1_000_000;
233
242
  +const maxUsers = 1_000_000n;
243
+ +const permission = 0o1_234_567;
244
+ +const mask = 0xab_cd_ef;
245
+ +const flags = 0b10100001_11000011;
246
+ +const ratio = 100_000.25;
247
+ +const large = 1e100_000;
234
248
  ```
235
249
 
236
- esupgrade transforms decimal integer and bigint literals with at least five digits.
237
- Fractional, exponential, hexadecimal, octal, binary, and already formatted literals are left unchanged.
250
+ Decimal and octal numerals are grouped by triplets; hex and binary by byte.
238
251
 
239
252
  #### `Array.slice(0)` → [Array spread [...]][mdn-spread]
240
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.20.1",
3
+ "version": "2025.21.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,6 +1,33 @@
1
1
  import { default as j } from "jscodeshift"
2
2
 
3
- const LARGE_DECIMAL_INTEGER_LITERAL = /^(?<digits>[1-9]\d{4,})(?<suffix>n)?$/
3
+ const MIN_DIGITS_FOR_GROUPING = 5
4
+ const TRANSFORMABLE_DECIMAL_INTEGER_LITERAL = new RegExp(
5
+ `^(?<digits>[1-9]\\d{${MIN_DIGITS_FOR_GROUPING - 1},})(?<suffix>n)?$`,
6
+ )
7
+ const TRANSFORMABLE_OCTAL_LITERAL =
8
+ /^(?<prefix>0[oO])(?<digits>[0-7]{4,})(?<suffix>n)?$/
9
+ const TRANSFORMABLE_HEX_LITERAL =
10
+ /^(?<prefix>0[xX])(?<digits>[0-9a-fA-F]{3,})(?<suffix>n)?$/
11
+ const TRANSFORMABLE_BINARY_LITERAL =
12
+ /^(?<prefix>0[bB])(?<digits>[01]{9,})(?<suffix>n)?$/
13
+ const FLOAT_LITERAL = /^(?<integer>\d+)\.(?<decimal>\d*)$/
14
+ const EXPONENTIAL_LITERAL =
15
+ /^(?:(?<mantissaInt>\d+)(?:\.(?<mantissaDec>\d*))?|\.(?<mantissaLeadingDec>\d+))(?<expMarker>[eE])(?<sign>[+-]?)(?<exponent>\d+)$/
16
+
17
+ /**
18
+ * Apply numeric separators to digit groups counted from the right.
19
+ *
20
+ * @param {string} digits - Digit string without separators.
21
+ * @param {number} groupSize - Number of characters per group.
22
+ * @returns {string} Digits with `_` inserted between right-aligned groups.
23
+ */
24
+ function applyDigitGroupSep(digits, groupSize) {
25
+ const firstGroupLength = digits.length % groupSize || groupSize
26
+
27
+ return digits.length > groupSize
28
+ ? `${digits.slice(0, firstGroupLength)}_${applyDigitGroupSep(digits.slice(firstGroupLength), groupSize)}`
29
+ : digits
30
+ }
4
31
 
5
32
  /**
6
33
  * Return a decimal integer literal with numeric separators when applicable.
@@ -8,8 +35,8 @@ const LARGE_DECIMAL_INTEGER_LITERAL = /^(?<digits>[1-9]\d{4,})(?<suffix>n)?$/
8
35
  * @param {string} rawLiteral - Raw literal source text.
9
36
  * @returns {string | null} Formatted literal or null when no change applies.
10
37
  */
11
- function formatNumericLiteral(rawLiteral) {
12
- const match = rawLiteral.match(LARGE_DECIMAL_INTEGER_LITERAL)
38
+ function formatDecimalIntegerLiteral(rawLiteral) {
39
+ const match = rawLiteral.match(TRANSFORMABLE_DECIMAL_INTEGER_LITERAL)
13
40
 
14
41
  if (!match?.groups) {
15
42
  return null
@@ -17,7 +44,147 @@ function formatNumericLiteral(rawLiteral) {
17
44
 
18
45
  const { digits, suffix = "" } = match.groups
19
46
 
20
- return `${digits.replace(/\B(?=(\d{3})+(?!\d))/g, "_")}${suffix}`
47
+ return `${applyDigitGroupSep(digits, 3)}${suffix}`
48
+ }
49
+
50
+ /**
51
+ * Return an octal literal with triplet numeric separators when applicable.
52
+ *
53
+ * @param {string} rawLiteral - Raw literal source text.
54
+ * @returns {string | null} Formatted literal or null when no change applies.
55
+ */
56
+ function formatOctalLiteral(rawLiteral) {
57
+ const match = rawLiteral.match(TRANSFORMABLE_OCTAL_LITERAL)
58
+
59
+ if (!match?.groups) {
60
+ return null
61
+ }
62
+
63
+ const { prefix, digits, suffix = "" } = match.groups
64
+
65
+ return `${prefix}${applyDigitGroupSep(digits, 3)}${suffix}`
66
+ }
67
+
68
+ /**
69
+ * Return a hex literal with byte-level numeric separators when applicable.
70
+ *
71
+ * @param {string} rawLiteral - Raw literal source text.
72
+ * @returns {string | null} Formatted literal or null when no change applies.
73
+ */
74
+ function formatHexLiteral(rawLiteral) {
75
+ const match = rawLiteral.match(TRANSFORMABLE_HEX_LITERAL)
76
+
77
+ if (!match?.groups) {
78
+ return null
79
+ }
80
+
81
+ const { prefix, digits, suffix = "" } = match.groups
82
+
83
+ // Insert `_` before each group of 2 hex chars counted from the right.
84
+ return `${prefix}${applyDigitGroupSep(digits, 2)}${suffix}`
85
+ }
86
+
87
+ /**
88
+ * Return a binary literal with byte-level numeric separators when applicable.
89
+ *
90
+ * @param {string} rawLiteral - Raw literal source text.
91
+ * @returns {string | null} Formatted literal or null when no change applies.
92
+ */
93
+ function formatBinaryLiteral(rawLiteral) {
94
+ const match = rawLiteral.match(TRANSFORMABLE_BINARY_LITERAL)
95
+
96
+ if (!match?.groups) {
97
+ return null
98
+ }
99
+
100
+ const { prefix, digits, suffix = "" } = match.groups
101
+
102
+ // Insert `_` before each group of 8 bits counted from the right.
103
+ return `${prefix}${applyDigitGroupSep(digits, 8)}${suffix}`
104
+ }
105
+
106
+ /**
107
+ * Return a float literal with thousands separators on the integer part when applicable.
108
+ *
109
+ * @param {string} rawLiteral - Raw literal source text.
110
+ * @returns {string | null} Formatted literal or null when no change applies.
111
+ */
112
+ function formatFloatLiteral(rawLiteral) {
113
+ const match = rawLiteral.match(FLOAT_LITERAL)
114
+
115
+ if (!match?.groups) {
116
+ return null
117
+ }
118
+
119
+ const { integer, decimal } = match.groups
120
+
121
+ if (integer.length < MIN_DIGITS_FOR_GROUPING) {
122
+ return null
123
+ }
124
+
125
+ return `${applyDigitGroupSep(integer, 3)}.${decimal}`
126
+ }
127
+
128
+ /**
129
+ * Return an exponential literal with thousands separators on large parts when applicable.
130
+ *
131
+ * @param {string} rawLiteral - Raw literal source text.
132
+ * @returns {string | null} Formatted literal or null when no change applies.
133
+ */
134
+ function formatExponentialLiteral(rawLiteral) {
135
+ const match = rawLiteral.match(EXPONENTIAL_LITERAL)
136
+
137
+ if (!match?.groups) {
138
+ return null
139
+ }
140
+
141
+ const {
142
+ mantissaInt = "",
143
+ mantissaDec,
144
+ mantissaLeadingDec,
145
+ expMarker,
146
+ sign,
147
+ exponent,
148
+ } = match.groups
149
+
150
+ const formattedMantissaInt =
151
+ mantissaInt.length >= MIN_DIGITS_FOR_GROUPING
152
+ ? applyDigitGroupSep(mantissaInt, 3)
153
+ : mantissaInt
154
+ const formattedExponent =
155
+ exponent.length >= MIN_DIGITS_FOR_GROUPING
156
+ ? applyDigitGroupSep(exponent, 3)
157
+ : exponent
158
+
159
+ if (formattedMantissaInt === mantissaInt && formattedExponent === exponent) {
160
+ return null
161
+ }
162
+
163
+ const mantissa =
164
+ typeof mantissaLeadingDec === "string"
165
+ ? `.${mantissaLeadingDec}`
166
+ : typeof mantissaDec === "string"
167
+ ? `${formattedMantissaInt}.${mantissaDec}`
168
+ : formattedMantissaInt
169
+
170
+ return `${mantissa}${expMarker}${sign}${formattedExponent}`
171
+ }
172
+
173
+ /**
174
+ * Return a numeric literal with separators when applicable.
175
+ *
176
+ * @param {string} rawLiteral - Raw literal source text.
177
+ * @returns {string | null} Formatted literal or null when no change applies.
178
+ */
179
+ function formatLiteral(rawLiteral) {
180
+ return (
181
+ formatDecimalIntegerLiteral(rawLiteral) ??
182
+ formatOctalLiteral(rawLiteral) ??
183
+ formatHexLiteral(rawLiteral) ??
184
+ formatBinaryLiteral(rawLiteral) ??
185
+ formatFloatLiteral(rawLiteral) ??
186
+ formatExponentialLiteral(rawLiteral)
187
+ )
21
188
  }
22
189
 
23
190
  /**
@@ -29,7 +196,7 @@ function formatNumericLiteral(rawLiteral) {
29
196
  function updateNumericLiteral(literalNode) {
30
197
  const formattedLiteral =
31
198
  typeof literalNode.extra?.raw === "string"
32
- ? formatNumericLiteral(literalNode.extra.raw)
199
+ ? formatLiteral(literalNode.extra.raw)
33
200
  : null
34
201
 
35
202
  if (!formattedLiteral) {
@@ -45,7 +212,7 @@ function updateNumericLiteral(literalNode) {
45
212
  }
46
213
 
47
214
  /**
48
- * Transform large decimal integer literals to use numeric separators.
215
+ * Transform numeric literals to use numeric separators.
49
216
  *
50
217
  * @param {import("jscodeshift").Collection} root - The root AST collection.
51
218
  * @returns {boolean} True if code was modified.
@@ -40,10 +40,240 @@ suite("widely-available", () => {
40
40
  assert(!result.modified)
41
41
  })
42
42
 
43
- test("skip non-decimal literals", () => {
44
- const result = transform(
45
- `const hexMask = 0xabcdef; const exponent = 1e12; const ratio = 10000.25;`,
46
- )
43
+ test("skip small float and short exponent literals", () => {
44
+ const result = transform(`const ratio = 1000.25; const exponent = 1e12;`)
45
+
46
+ assert(!result.modified)
47
+ })
48
+
49
+ test("transform float literals", () => {
50
+ const result = transform(`const ratio = 100000.25;`)
51
+
52
+ assert.match(result.code, /100_000\.25/)
53
+ assert(result.modified)
54
+ })
55
+
56
+ test("transform float literals with exact five-digit integer part", () => {
57
+ const result = transform(`const ratio = 10000.5;`)
58
+
59
+ assert.match(result.code, /10_000\.5/)
60
+ assert(result.modified)
61
+ })
62
+
63
+ test("transform float literals without decimal digits", () => {
64
+ const result = transform(`const ratio = 10000.;`)
65
+
66
+ assert.match(result.code, /10_000\./)
67
+ assert(result.modified)
68
+ })
69
+
70
+ test("skip float literals with short integer part", () => {
71
+ const result = transform(`const ratio = 1000.5;`)
72
+
73
+ assert(!result.modified)
74
+ })
75
+
76
+ test("transform exponential literals with large exponent", () => {
77
+ const result = transform(`const n = 1e100000;`)
78
+
79
+ assert.match(result.code, /1e100_000/)
80
+ assert(result.modified)
81
+ })
82
+
83
+ test("transform exponential literals with large mantissa", () => {
84
+ const result = transform(`const n = 10000e12;`)
85
+
86
+ assert.match(result.code, /10_000e12/)
87
+ assert(result.modified)
88
+ })
89
+
90
+ test("transform exponential literals with large mantissa and exponent", () => {
91
+ const result = transform(`const n = 10000e100000;`)
92
+
93
+ assert.match(result.code, /10_000e100_000/)
94
+ assert(result.modified)
95
+ })
96
+
97
+ test("transform exponential literals with float mantissa and large exponent", () => {
98
+ const result = transform(`const n = 1.5e100000;`)
99
+
100
+ assert.match(result.code, /1\.5e100_000/)
101
+ assert(result.modified)
102
+ })
103
+
104
+ test("transform exponential literals with leading-dot mantissa", () => {
105
+ const result = transform(`const n = .5e100000;`)
106
+
107
+ assert.match(result.code, /\.5e100_000/)
108
+ assert(result.modified)
109
+ })
110
+
111
+ test("transform exponential literals with trailing-dot mantissa", () => {
112
+ const result = transform(`const n = 10000.e100000;`)
113
+
114
+ assert.match(result.code, /10_000\.e100_000/)
115
+ assert(result.modified)
116
+ })
117
+
118
+ test("transform exponential literals with signed exponent", () => {
119
+ const result = transform(`const n = 1e+100000;`)
120
+
121
+ assert.match(result.code, /1e\+100_000/)
122
+ assert(result.modified)
123
+ })
124
+
125
+ test("transform exponential literals with negative exponent", () => {
126
+ const result = transform(`const n = 1e-100000;`)
127
+
128
+ assert.match(result.code, /1e-100_000/)
129
+ assert(result.modified)
130
+ })
131
+
132
+ test("transform float literals with leading-zero decimal part", () => {
133
+ const result = transform(`const ratio = 10000.05;`)
134
+
135
+ assert.match(result.code, /10_000\.05/)
136
+ assert(result.modified)
137
+ })
138
+
139
+ test("transform hex literals", () => {
140
+ const result = transform(`const mask = 0xabcdef;`)
141
+
142
+ assert.match(result.code, /0xab_cd_ef/)
143
+ assert(result.modified)
144
+ })
145
+
146
+ test("transform octal literals", () => {
147
+ const result = transform(`const permission = 0o1234567;`)
148
+
149
+ assert.match(result.code, /0o1_234_567/)
150
+ assert(result.modified)
151
+ })
152
+
153
+ test("transform uppercase octal literals", () => {
154
+ const result = transform(`const permission = 0O1234567;`)
155
+
156
+ assert.match(result.code, /0O1_234_567/)
157
+ assert(result.modified)
158
+ })
159
+
160
+ test("transform octal bigint literals", () => {
161
+ const result = transform(`const permission = 0o1234567n;`)
162
+
163
+ assert.match(result.code, /0o1_234_567n/)
164
+ assert(result.modified)
165
+ })
166
+
167
+ test("skip short octal literals", () => {
168
+ const result = transform(`const permission = 0o777;`)
169
+
170
+ assert(!result.modified)
171
+ })
172
+
173
+ test("skip already formatted octal literals", () => {
174
+ const result = transform(`const permission = 0o1_234_567;`)
175
+
176
+ assert(!result.modified)
177
+ })
178
+
179
+ test("transform uppercase hex literals", () => {
180
+ const result = transform(`const mask = 0xABCDEF;`)
181
+
182
+ assert.match(result.code, /0xAB_CD_EF/)
183
+ assert(result.modified)
184
+ })
185
+
186
+ test("transform hex literals with odd digit count", () => {
187
+ const result = transform(`const mask = 0xabcde;`)
188
+
189
+ assert.match(result.code, /0xa_bc_de/)
190
+ assert(result.modified)
191
+ })
192
+
193
+ test("transform hex bigint literals", () => {
194
+ const result = transform(`const id = 0xabcdefn;`)
195
+
196
+ assert.match(result.code, /0xab_cd_efn/)
197
+ assert(result.modified)
198
+ })
199
+
200
+ test("skip short hex literals", () => {
201
+ const result = transform(`const byte = 0xff;`)
202
+
203
+ assert(!result.modified)
204
+ })
205
+
206
+ test("skip already formatted hex literals", () => {
207
+ const result = transform(`const mask = 0xab_cd_ef;`)
208
+
209
+ assert(!result.modified)
210
+ })
211
+
212
+ test("transform binary literals", () => {
213
+ const result = transform(`const flags = 0b1010000111000011;`)
214
+
215
+ assert.match(result.code, /0b10100001_11000011/)
216
+ assert(result.modified)
217
+ })
218
+
219
+ test("transform binary literals with partial leading byte", () => {
220
+ const result = transform(`const flags = 0b100000001;`)
221
+
222
+ assert.match(result.code, /0b1_00000001/)
223
+ assert(result.modified)
224
+ })
225
+
226
+ test("transform binary bigint literals", () => {
227
+ const result = transform(`const flags = 0b1010000111000011n;`)
228
+
229
+ assert.match(result.code, /0b10100001_11000011n/)
230
+ assert(result.modified)
231
+ })
232
+
233
+ test("skip short binary literals", () => {
234
+ const result = transform(`const byte = 0b11111111;`)
235
+
236
+ assert(!result.modified)
237
+ })
238
+
239
+ test("skip already formatted binary literals", () => {
240
+ const result = transform(`const flags = 0b10100001_11000011;`)
241
+
242
+ assert(!result.modified)
243
+ })
244
+
245
+ test("skip hex literals with non-byte grouping", () => {
246
+ const result = transform(`const mask = 0xabc_def;`)
247
+
248
+ assert(!result.modified)
249
+ })
250
+
251
+ test("skip binary literals with non-byte grouping", () => {
252
+ const result = transform(`const flags = 0b1010_0001_1100_0011;`)
253
+
254
+ assert(!result.modified)
255
+ })
256
+
257
+ test("skip decimal literals with non-thousand grouping", () => {
258
+ const result = transform(`const n = 1_0000_000;`)
259
+
260
+ assert(!result.modified)
261
+ })
262
+
263
+ test("skip octal literals with non-triplet grouping", () => {
264
+ const result = transform(`const permission = 0o12_34567;`)
265
+
266
+ assert(!result.modified)
267
+ })
268
+
269
+ test("skip float literals with non-standard existing separators", () => {
270
+ const result = transform(`const ratio = 100_00.25;`)
271
+
272
+ assert(!result.modified)
273
+ })
274
+
275
+ test("skip exponential literals with non-standard existing separators", () => {
276
+ const result = transform(`const n = 1_0000e12;`)
47
277
 
48
278
  assert(!result.modified)
49
279
  })