ts-codemod-lib 3.5.1 → 4.0.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.
Files changed (59) hide show
  1. package/README.md +7 -3
  2. package/dist/functions/ast-transformers/append-as-const.d.mts +20 -12
  3. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  4. package/dist/functions/ast-transformers/append-as-const.mjs +170 -88
  5. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  6. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +25 -6
  7. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  8. package/dist/functions/ast-transformers/convert-to-readonly.mjs +272 -255
  9. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  10. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
  11. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
  12. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
  13. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
  14. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
  15. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  16. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  17. package/dist/functions/ast-transformers/transform-source-code.mjs +69 -69
  18. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  19. package/dist/functions/constants/index.d.mts +1 -0
  20. package/dist/functions/constants/index.d.mts.map +1 -1
  21. package/dist/functions/constants/index.mjs +1 -0
  22. package/dist/functions/constants/index.mjs.map +1 -1
  23. package/dist/functions/constants/mutable-prefixes.d.mts +23 -0
  24. package/dist/functions/constants/mutable-prefixes.d.mts.map +1 -0
  25. package/dist/functions/constants/mutable-prefixes.mjs +28 -0
  26. package/dist/functions/constants/mutable-prefixes.mjs.map +1 -0
  27. package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
  28. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  29. package/dist/functions/functions/is-readonly-node.d.mts +2 -2
  30. package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
  31. package/dist/functions/functions/is-readonly-node.mjs +13 -13
  32. package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
  33. package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
  34. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  35. package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
  36. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  37. package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
  38. package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
  39. package/dist/functions/utils/replace-node-with-text.mjs +0 -1
  40. package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
  41. package/package.json +6 -6
  42. package/src/functions/ast-transformers/append-as-const.mts +319 -174
  43. package/src/functions/ast-transformers/append-as-const.test.mts +118 -7
  44. package/src/functions/ast-transformers/convert-to-readonly.mts +568 -489
  45. package/src/functions/ast-transformers/convert-to-readonly.test.mts +381 -74
  46. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
  47. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
  48. package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
  49. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
  50. package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
  51. package/src/functions/ast-transformers/transform-source-code.mts +110 -110
  52. package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
  53. package/src/functions/constants/index.mts +1 -0
  54. package/src/functions/constants/mutable-prefixes.mts +28 -0
  55. package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
  56. package/src/functions/functions/is-readonly-node.mts +25 -25
  57. package/src/functions/functions/wrap-with-parentheses.mts +138 -52
  58. package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
  59. package/src/functions/utils/replace-node-with-text.mts +10 -11
@@ -9,15 +9,15 @@ const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
9
9
  /**
10
10
  * Appends `!` to the index accesses that turning on the
11
11
  * [`noUncheckedIndexedAccess`](https://www.typescriptlang.org/tsconfig/#noUncheckedIndexedAccess)
12
- * compiler option would turn into type errors, as a stopgap while the option
13
- * is being enabled.
12
+ * compiler option would turn into type errors, as a stopgap while the option is
13
+ * being enabled.
14
14
  *
15
15
  * The rewrite is driven by the type checker rather than by syntax: the file is
16
16
  * checked twice, once with `noUncheckedIndexedAccess` off and once with it on,
17
17
  * and `!` is appended only where the option is what added `undefined` to the
18
18
  * expression's type. Consequently an index whose presence the type already
19
- * guarantees keeps its access untouched — `[T, T][1]`, `[T, T, ...T[]][0]`,
20
- * and `({ a: T })['a']` are all left as they are, as is an element type that
19
+ * guarantees keeps its access untouched — `[T, T][1]`, `[T, T, ...T[]][0]`, and
20
+ * `({ a: T })['a']` are all left as they are, as is an element type that
21
21
  * contained `undefined` to begin with.
22
22
  *
23
23
  * Both spellings of an index read are covered, because the option widens both:
@@ -31,10 +31,10 @@ const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
31
31
  * on its own — which is how {@link transformSourceCode} drives every
32
32
  * transformer here — resolves neither a type from another module nor a
33
33
  * narrowing that depends on an imported type guard, and the latter produces
34
- * assertions a whole-program check does not need. Most of those are reported
35
- * by `@typescript-eslint/no-unnecessary-type-assertion`, whose fixer removes
36
- * them; what no rule can see is an assertion that makes a later check dead,
37
- * as the `if` of `const v = xs[0]!; if (v === undefined) …` now is.
34
+ * assertions a whole-program check does not need. Most of those are reported by
35
+ * `@typescript-eslint/no-unnecessary-type-assertion`, whose fixer removes them;
36
+ * what no rule can see is an assertion that makes a later check dead, as the
37
+ * `if` of `const v = xs[0]!; if (v === undefined) …` now is.
38
38
  *
39
39
  * @example
40
40
  *
@@ -91,9 +91,9 @@ export const enableNoUncheckedIndexedAccessTransformer = (
91
91
  return;
92
92
  }
93
93
 
94
- // One text edit for the whole file: `insertText` forgets every
95
- // descendant node, so inserting one `!` at a time would reparse the
96
- // file once per assertion.
94
+ // One text edit for the whole file: `insertText` forgets every descendant
95
+ // node, so inserting one `!` at a time would reparse the file once per
96
+ // assertion.
97
97
  sourceAst.replaceText(
98
98
  [0, sourceAst.getEnd()],
99
99
  insertNonNullAssertions(sourceAst.getFullText(), positions),
@@ -134,102 +134,6 @@ type EnableNoUncheckedIndexedAccessTransformerOptionsInternal = DeepReadonly<{
134
134
  type IndexReadExpression =
135
135
  tsm.ElementAccessExpression | tsm.PropertyAccessExpression;
136
136
 
137
- /**
138
- * Returns the positions at which a `!` has to be inserted, i.e. the end
139
- * offsets of the reads whose type `noUncheckedIndexedAccess` widens with
140
- * `undefined`.
141
- */
142
- const collectAssertionPositions = (
143
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
144
- sourceAst: tsm.SourceFile,
145
- options: EnableNoUncheckedIndexedAccessTransformerOptionsInternal,
146
- ): readonly number[] => {
147
- const candidates: readonly IndexReadExpression[] = [
148
- ...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.ElementAccessExpression),
149
- ...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.PropertyAccessExpression),
150
- ].filter(
151
- (node) => isAssertableReadPosition(node) && !isIgnoredByComment(node),
152
- );
153
-
154
- if (Arr.isEmpty(candidates)) {
155
- return [];
156
- }
157
-
158
- const project = sourceAst.getProject();
159
-
160
- const originalOptions = project.getCompilerOptions();
161
-
162
- try {
163
- // With the option on first, because that narrows the candidates down to
164
- // the few reads worth checking a second time. `strictNullChecks` comes
165
- // with it: `noUncheckedIndexedAccess` does nothing without it, and the
166
- // project handed to the transformer may not have it on.
167
- project.compilerOptions.set({
168
- strictNullChecks: true,
169
- noUncheckedIndexedAccess: true,
170
- });
171
-
172
- // The type asked is the access's own, so an `as`, `satisfies` or `!` on
173
- // its object is already in it. That is the type the option's error is
174
- // reported against, and the `!` appended has no run-time effect for the
175
- // unwrapped object to contradict.
176
- const widened = candidates.filter(
177
- (node) =>
178
- typeIncludesUndefined(node.getType()) &&
179
- !(
180
- options.applyLevel === 'avoidWhereUndefinedIsAllowed' &&
181
- contextAcceptsUndefined(node)
182
- ),
183
- );
184
-
185
- if (Arr.isEmpty(widened)) {
186
- return [];
187
- }
188
-
189
- project.compilerOptions.set({ noUncheckedIndexedAccess: false });
190
-
191
- // What is possibly `undefined` without the option too is not the option's
192
- // doing — the element type says so, or the expression is erroneous (an
193
- // unresolved import resolves to `any`, which is never widened).
194
- return widened
195
- .filter((node) => !typeIncludesUndefined(node.getType()))
196
- .map((node) => {
197
- options.debugPrint(`${node.getText()} -> ${node.getText()}!`);
198
-
199
- return node.getEnd();
200
- });
201
- } finally {
202
- // `set` merges, so the two options overridden above have to be named
203
- // explicitly: they are absent from `originalOptions` when the project did
204
- // not carry them, and would otherwise stay overridden for whichever
205
- // transformer runs next.
206
- project.compilerOptions.set({
207
- ...originalOptions,
208
- strictNullChecks: originalOptions.strictNullChecks,
209
- noUncheckedIndexedAccess: originalOptions.noUncheckedIndexedAccess,
210
- });
211
- }
212
- };
213
-
214
- const insertNonNullAssertions = (
215
- fullText: string,
216
- positions: readonly number[],
217
- ): string => {
218
- const mut_parts: string[] = [];
219
-
220
- let mut_restStart: number = 0;
221
-
222
- for (const position of positions.toSorted((a, b) => a - b)) {
223
- mut_parts.push(fullText.slice(mut_restStart, position), '!');
224
-
225
- mut_restStart = position;
226
- }
227
-
228
- mut_parts.push(fullText.slice(mut_restStart));
229
-
230
- return mut_parts.join('');
231
- };
232
-
233
137
  const typeIncludesUndefined = (
234
138
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
235
139
  type: tsm.Type,
@@ -238,9 +142,9 @@ const typeIncludesUndefined = (
238
142
  (type.isUnion() && type.getUnionTypes().some((t) => t.isUndefined()));
239
143
 
240
144
  /**
241
- * Whether the position the access sits in already accepts `undefined`, in
242
- * which case `noUncheckedIndexedAccess` reports nothing there and the
243
- * assertion would only be noise.
145
+ * Whether the position the access sits in already accepts `undefined`, in which
146
+ * case `noUncheckedIndexedAccess` reports nothing there and the assertion would
147
+ * only be noise.
244
148
  */
245
149
  const contextAcceptsUndefined = (
246
150
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -276,38 +180,6 @@ const isIgnoredByComment = (
276
180
  );
277
181
  };
278
182
 
279
- /**
280
- * Whether a `!` appended to this access would be both syntactically valid and
281
- * meaningful.
282
- *
283
- * Note that most write positions need no rule of their own: TypeScript gives
284
- * the target of an assignment the element type without `undefined`, so the
285
- * type comparison in {@link collectAssertionPositions} already passes them
286
- * over. The exceptions handled here are the ones it does widen.
287
- *
288
- * The position is that of the outermost parenthesis around the access:
289
- * `(xs[0]) === undefined` tests the value exactly as `xs[0] === undefined`
290
- * does, while the `!` still goes right after the access, inside them.
291
- */
292
- const isAssertableReadPosition = (
293
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
294
- node: IndexReadExpression,
295
- ): boolean => {
296
- const outer = outermostParenthesized(node);
297
-
298
- const parent = outer.getParent();
299
-
300
- if (parent === undefined) {
301
- return false;
302
- }
303
-
304
- return (
305
- !isAlreadyAssertedOrGuarded(node, outer, parent) &&
306
- !isWriteTarget(outer, parent) &&
307
- !isTestedForAbsence(outer, parent)
308
- );
309
- };
310
-
311
183
  /** `node`, or the outermost of the parentheses written around it. */
312
184
  const outermostParenthesized = (node: tsm.Node): tsm.Node => {
313
185
  const parent = node.getParent();
@@ -317,12 +189,38 @@ const outermostParenthesized = (node: tsm.Node): tsm.Node => {
317
189
  : node;
318
190
  };
319
191
 
192
+ /**
193
+ * Whether the read sits in an optional chain that can short-circuit before it,
194
+ * as the `xs[0]?.toFixed` of `xs[0]?.toFixed()` does. `undefined` in such a
195
+ * type comes from the chain rather than from the index, and the chain already
196
+ * accounts for it; asserting there would assert away the author's own `?.`.
197
+ *
198
+ * Parentheses end a chain (`(a?.b).c` is not one), which is why the walk stops
199
+ * at anything that is not a chain link.
200
+ */
201
+ const isInOptionalChain = (node: tsm.Node): boolean => {
202
+ if (
203
+ node.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
204
+ node.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
205
+ node.isKind(tsm.SyntaxKind.CallExpression)
206
+ ) {
207
+ return (
208
+ node.hasQuestionDotToken() || isInOptionalChain(node.getExpression())
209
+ );
210
+ }
211
+
212
+ return (
213
+ node.isKind(tsm.SyntaxKind.NonNullExpression) &&
214
+ isInOptionalChain(node.getExpression())
215
+ );
216
+ };
217
+
320
218
  /**
321
219
  * `xs[0]!`, `xs[0] as T`, `<T>xs[0]`, `xs[0]?.foo`, `xs[0]?.()`, `xs[0]?.[1]`.
322
220
  *
323
- * `outer` is `node` with its parentheses (see
324
- * {@link outermostParenthesized}); a chain continues from the access itself
325
- * but is guarded from the outside of the parentheses (`(xs[0])?.foo`).
221
+ * `outer` is `node` with its parentheses (see {@link outermostParenthesized});
222
+ * a chain continues from the access itself but is guarded from the outside of
223
+ * the parentheses (`(xs[0])?.foo`).
326
224
  */
327
225
  const isAlreadyAssertedOrGuarded = (
328
226
  node: tsm.Node,
@@ -341,8 +239,8 @@ const isAlreadyAssertedOrGuarded = (
341
239
  return true;
342
240
  }
343
241
 
344
- // Only the object of the optional chain is guarded; an index argument such
345
- // as the `xs[0]` of `m?.[xs[0]]` is not.
242
+ // Only the object of the optional chain is guarded; an index argument such as
243
+ // the `xs[0]` of `m?.[xs[0]]` is not.
346
244
  return (
347
245
  (parent.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
348
246
  parent.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
@@ -353,36 +251,19 @@ const isAlreadyAssertedOrGuarded = (
353
251
  };
354
252
 
355
253
  /**
356
- * Whether the read sits in an optional chain that can short-circuit before it,
357
- * as the `xs[0]?.toFixed` of `xs[0]?.toFixed()` does. `undefined` in such a
358
- * type comes from the chain rather than from the index, and the chain already
359
- * accounts for it; asserting there would assert away the author's own `?.`.
360
- *
361
- * Parentheses end a chain (`(a?.b).c` is not one), which is why the walk stops
362
- * at anything that is not a chain link.
254
+ * `=` through `^=`, i.e. every assignment operator. TypeScript keeps them in
255
+ * one contiguous block of {@link tsm.SyntaxKind} and marks its ends, which is
256
+ * how the compiler itself recognizes them.
363
257
  */
364
- const isInOptionalChain = (node: tsm.Node): boolean => {
365
- if (
366
- node.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
367
- node.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
368
- node.isKind(tsm.SyntaxKind.CallExpression)
369
- ) {
370
- return (
371
- node.hasQuestionDotToken() || isInOptionalChain(node.getExpression())
372
- );
373
- }
374
-
375
- return (
376
- node.isKind(tsm.SyntaxKind.NonNullExpression) &&
377
- isInOptionalChain(node.getExpression())
378
- );
379
- };
258
+ const isAssignmentOperatorKind = (kind: tsm.SyntaxKind): boolean =>
259
+ tsm.SyntaxKind.FirstAssignment <= kind &&
260
+ kind <= tsm.SyntaxKind.LastAssignment;
380
261
 
381
262
  /**
382
263
  * Positions that write the value rather than read it, where `!` cannot be
383
- * appended at all: an assignment target is not an expression (`xs[0]! = 1`
384
- * does not parse as an assignment), and `delete xs[0]!` and `xs[0]!++` are
385
- * equally invalid.
264
+ * appended at all: an assignment target is not an expression (`xs[0]! = 1` does
265
+ * not parse as an assignment), and `delete xs[0]!` and `xs[0]!++` are equally
266
+ * invalid.
386
267
  *
387
268
  * `xs[0] += 1` is the one case this leaves a type error behind: its left-hand
388
269
  * side is read as well as written, so `noUncheckedIndexedAccess` reports it,
@@ -465,13 +346,131 @@ const isTestedForAbsence = (node: tsm.Node, parent: tsm.Node): boolean => {
465
346
  };
466
347
 
467
348
  /**
468
- * `=` through `^=`, i.e. every assignment operator. TypeScript keeps them in
469
- * one contiguous block of {@link tsm.SyntaxKind} and marks its ends, which is
470
- * how the compiler itself recognizes them.
349
+ * Whether a `!` appended to this access would be both syntactically valid and
350
+ * meaningful.
351
+ *
352
+ * Note that most write positions need no rule of their own: TypeScript gives
353
+ * the target of an assignment the element type without `undefined`, so the type
354
+ * comparison in {@link collectAssertionPositions} already passes them over. The
355
+ * exceptions handled here are the ones it does widen.
356
+ *
357
+ * The position is that of the outermost parenthesis around the access:
358
+ * `(xs[0]) === undefined` tests the value exactly as `xs[0] === undefined`
359
+ * does, while the `!` still goes right after the access, inside them.
471
360
  */
472
- const isAssignmentOperatorKind = (kind: tsm.SyntaxKind): boolean =>
473
- tsm.SyntaxKind.FirstAssignment <= kind &&
474
- kind <= tsm.SyntaxKind.LastAssignment;
361
+ const isAssertableReadPosition = (
362
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
363
+ node: IndexReadExpression,
364
+ ): boolean => {
365
+ const outer = outermostParenthesized(node);
366
+
367
+ const parent = outer.getParent();
368
+
369
+ if (parent === undefined) {
370
+ return false;
371
+ }
372
+
373
+ return (
374
+ !isAlreadyAssertedOrGuarded(node, outer, parent) &&
375
+ !isWriteTarget(outer, parent) &&
376
+ !isTestedForAbsence(outer, parent)
377
+ );
378
+ };
379
+
380
+ /**
381
+ * Returns the positions at which a `!` has to be inserted, i.e. the end offsets
382
+ * of the reads whose type `noUncheckedIndexedAccess` widens with `undefined`.
383
+ */
384
+ const collectAssertionPositions = (
385
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
386
+ sourceAst: tsm.SourceFile,
387
+ options: EnableNoUncheckedIndexedAccessTransformerOptionsInternal,
388
+ ): readonly number[] => {
389
+ const candidates: readonly IndexReadExpression[] = [
390
+ ...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.ElementAccessExpression),
391
+ ...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.PropertyAccessExpression),
392
+ ].filter(
393
+ (node) => isAssertableReadPosition(node) && !isIgnoredByComment(node),
394
+ );
395
+
396
+ if (Arr.isEmpty(candidates)) {
397
+ return [];
398
+ }
399
+
400
+ const project = sourceAst.getProject();
401
+
402
+ const originalOptions = project.getCompilerOptions();
403
+
404
+ try {
405
+ // With the option on first, because that narrows the candidates down to the
406
+ // few reads worth checking a second time. `strictNullChecks` comes with it:
407
+ // `noUncheckedIndexedAccess` does nothing without it, and the project
408
+ // handed to the transformer may not have it on.
409
+ project.compilerOptions.set({
410
+ strictNullChecks: true,
411
+ noUncheckedIndexedAccess: true,
412
+ });
413
+
414
+ // The type asked is the access's own, so an `as`, `satisfies` or `!` on its
415
+ // object is already in it. That is the type the option's error is reported
416
+ // against, and the `!` appended has no run-time effect for the unwrapped
417
+ // object to contradict.
418
+ const widened = candidates.filter(
419
+ (node) =>
420
+ typeIncludesUndefined(node.getType()) &&
421
+ !(
422
+ options.applyLevel === 'avoidWhereUndefinedIsAllowed' &&
423
+ contextAcceptsUndefined(node)
424
+ ),
425
+ );
426
+
427
+ if (Arr.isEmpty(widened)) {
428
+ return [];
429
+ }
430
+
431
+ project.compilerOptions.set({ noUncheckedIndexedAccess: false });
432
+
433
+ // What is possibly `undefined` without the option too is not the option's
434
+ // doing — the element type says so, or the expression is erroneous (an
435
+ // unresolved import resolves to `any`, which is never widened).
436
+ return widened
437
+ .filter((node) => !typeIncludesUndefined(node.getType()))
438
+ .map((node) => {
439
+ options.debugPrint(`${node.getText()} -> ${node.getText()}!`);
440
+
441
+ return node.getEnd();
442
+ });
443
+ } finally {
444
+ // `set` merges, so the two options overridden above have to be named
445
+ // explicitly: they are absent from `originalOptions` when the project did
446
+ // not carry them, and would otherwise stay overridden for whichever
447
+ // transformer runs next.
448
+ project.compilerOptions.set({
449
+ ...originalOptions,
450
+ strictNullChecks: originalOptions.strictNullChecks,
451
+ noUncheckedIndexedAccess: originalOptions.noUncheckedIndexedAccess,
452
+ });
453
+ }
454
+ };
455
+
456
+ const insertNonNullAssertions = (
457
+ fullText: string,
458
+ positions: readonly number[],
459
+ ): string => {
460
+ const mut_parts: string[] = [];
461
+
462
+ let mut_restStart: number = 0;
463
+
464
+ for (const position of positions.toSorted((a, b) => a - b)) {
465
+ mut_parts.push(fullText.slice(mut_restStart, position), '!');
466
+
467
+ mut_restStart = position;
468
+ }
469
+
470
+ mut_parts.push(fullText.slice(mut_restStart));
471
+
472
+ return mut_parts.join('');
473
+ };
475
474
 
476
475
  const equalityOperatorKinds: readonly tsm.SyntaxKind[] = [
477
476
  tsm.SyntaxKind.EqualsEqualsToken,
@@ -34,6 +34,29 @@ const preamble = dedent`
34
34
  declare const k: string;
35
35
  `;
36
36
 
37
+ /**
38
+ * Runs the transformer over `code` in {@link project}, and returns what it made
39
+ * of it.
40
+ */
41
+ const transformInProject = (
42
+ code: string,
43
+ options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0],
44
+ ): string => {
45
+ const sourceAst = project.createSourceFile('source.ts', code, {
46
+ overwrite: true,
47
+ });
48
+
49
+ enableNoUncheckedIndexedAccessTransformer(options).transform(sourceAst);
50
+
51
+ return sourceAst.getFullText();
52
+ };
53
+
54
+ const formatter = (code: string): Promise<string> =>
55
+ prettier.format(code, {
56
+ parser: 'typescript',
57
+ plugins: [prettierPluginTypeScript, prettierPluginEstree],
58
+ });
59
+
37
60
  const testFn = async ({
38
61
  source,
39
62
  expected,
@@ -59,23 +82,6 @@ const testFn = async ({
59
82
  expect(transformed).toBe(expectedFormatted);
60
83
  };
61
84
 
62
- /**
63
- * Runs the transformer over `code` in {@link project}, and returns what it
64
- * made of it.
65
- */
66
- const transformInProject = (
67
- code: string,
68
- options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0],
69
- ): string => {
70
- const sourceAst = project.createSourceFile('source.ts', code, {
71
- overwrite: true,
72
- });
73
-
74
- enableNoUncheckedIndexedAccessTransformer(options).transform(sourceAst);
75
-
76
- return sourceAst.getFullText();
77
- };
78
-
79
85
  /**
80
86
  * The one project every case is checked in, rather than one project per case.
81
87
  *
@@ -87,14 +93,14 @@ const transformInProject = (
87
93
  * fraction of a millisecond. Sixty of those took `check:test:cov` past Vitest's
88
94
  * timeout on CI.
89
95
  *
90
- * Reusing the project is half of the answer and the smaller half — measured,
91
- * it takes a case from ~300 ms to ~260 ms, because what an edit invalidates is
92
- * the program, not the parsed library behind it. Naming the libraries the
93
- * cases actually need is the rest: together they bring a case to ~25 ms.
96
+ * Reusing the project is half of the answer and the smaller half — measured, it
97
+ * takes a case from ~300 ms to ~260 ms, because what an edit invalidates is the
98
+ * program, not the parsed library behind it. Naming the libraries the cases
99
+ * actually need is the rest: together they bring a case to ~25 ms.
94
100
  *
95
101
  * What that leaves out — `transformSourceCode` itself, and the transformer
96
- * against the full library — is what the tests at the bottom of this file
97
- * still cover.
102
+ * against the full library — is what the tests at the bottom of this file still
103
+ * cover.
98
104
  */
99
105
  const project = new tsm.Project({
100
106
  useInMemoryFileSystem: true,
@@ -107,12 +113,6 @@ const project = new tsm.Project({
107
113
  },
108
114
  });
109
115
 
110
- const formatter = (code: string): Promise<string> =>
111
- prettier.format(code, {
112
- parser: 'typescript',
113
- plugins: [prettierPluginTypeScript, prettierPluginEstree],
114
- });
115
-
116
116
  describe(enableNoUncheckedIndexedAccessTransformer, () => {
117
117
  describe('accesses the option widens with undefined', () => {
118
118
  test.each([
@@ -7,6 +7,12 @@ import * as prettier from 'prettier/standalone';
7
7
  import { replaceAnyWithUnknownTransformer } from './replace-any-with-unknown.mjs';
8
8
  import { transformSourceCode } from './transform-source-code.mjs';
9
9
 
10
+ const formatter = (code: string): Promise<string> =>
11
+ prettier.format(code, {
12
+ parser: 'typescript',
13
+ plugins: [prettierPluginTypeScript, prettierPluginEstree],
14
+ });
15
+
10
16
  const testFn = async ({
11
17
  source,
12
18
  expected,
@@ -33,12 +39,6 @@ const testFn = async ({
33
39
  expect(transformed).toBe(expectedFormatted);
34
40
  };
35
41
 
36
- const formatter = (code: string): Promise<string> =>
37
- prettier.format(code, {
38
- parser: 'typescript',
39
- plugins: [prettierPluginTypeScript, prettierPluginEstree],
40
- });
41
-
42
42
  describe(replaceAnyWithUnknownTransformer, () => {
43
43
  test.each([
44
44
  {