eslint-plugin-ts-data-forge 0.10.5 → 0.10.7

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 (107) hide show
  1. package/README.md +3 -3
  2. package/dist/plugin.d.mts +2 -2
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +5 -5
  7. package/dist/rules/ast-utils.d.mts +8 -9
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +3 -4
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/import-binding.d.mts +38 -0
  12. package/dist/rules/import-binding.d.mts.map +1 -0
  13. package/dist/rules/import-binding.mjs +53 -0
  14. package/dist/rules/import-binding.mjs.map +1 -0
  15. package/dist/rules/mutable-result.mjs +37 -37
  16. package/dist/rules/mutable-result.mjs.map +1 -1
  17. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  18. package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
  19. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-generate.d.mts +2 -2
  21. package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
  22. package/dist/rules/prefer-arr-generate.mjs +146 -136
  23. package/dist/rules/prefer-arr-generate.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  25. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
  26. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  27. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  28. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +15 -17
  29. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  30. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  31. package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
  32. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  33. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  34. package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
  35. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  37. package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
  38. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  39. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  40. package/dist/rules/prefer-arr-scan.mjs +9 -18
  41. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  42. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  43. package/dist/rules/prefer-arr-sum.mjs +16 -21
  44. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  45. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  46. package/dist/rules/prefer-arr-uniq.mjs +38 -43
  47. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  48. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  49. package/dist/rules/prefer-as-int.mjs +19 -26
  50. package/dist/rules/prefer-as-int.mjs.map +1 -1
  51. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  52. package/dist/rules/prefer-canonical-array-slicing.mjs +49 -16
  53. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  54. package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
  55. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  56. package/dist/rules/prefer-num-safe-parse-float.d.mts +1 -1
  57. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  58. package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -1
  59. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  60. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  61. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
  62. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  63. package/dist/rules/prefer-pipe-map-nullable.d.mts +2 -2
  64. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
  65. package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
  66. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
  67. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  68. package/dist/rules/prefer-range-for-loop.mjs +18 -24
  69. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  70. package/dist/rules/rules.d.mts +2 -2
  71. package/package.json +6 -6
  72. package/src/plugin.mts +4 -3
  73. package/src/rule-types.mts +5 -5
  74. package/src/rules/ast-utils.mts +14 -15
  75. package/src/rules/import-binding.mts +94 -0
  76. package/src/rules/mutable-result.mts +56 -56
  77. package/src/rules/no-unnecessary-type-guard.mts +203 -207
  78. package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
  79. package/src/rules/prefer-arr-generate.mts +222 -214
  80. package/src/rules/prefer-arr-generate.test.mts +490 -133
  81. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
  82. package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
  83. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
  84. package/src/rules/prefer-arr-is-max-length-array.mts +23 -36
  85. package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
  86. package/src/rules/prefer-arr-is-min-length-array.mts +23 -36
  87. package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
  88. package/src/rules/prefer-arr-is-non-empty.mts +17 -34
  89. package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
  90. package/src/rules/prefer-arr-scan.mts +17 -42
  91. package/src/rules/prefer-arr-scan.test.mts +55 -0
  92. package/src/rules/prefer-arr-sum.mts +22 -36
  93. package/src/rules/prefer-arr-uniq.mts +51 -62
  94. package/src/rules/prefer-arr-uniq.test.mts +46 -20
  95. package/src/rules/prefer-as-int.mts +27 -52
  96. package/src/rules/prefer-as-int.test.mts +79 -29
  97. package/src/rules/prefer-canonical-array-slicing.mts +101 -40
  98. package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
  99. package/src/rules/prefer-canonical-length-cast.mts +29 -29
  100. package/src/rules/prefer-canonical-length-guard.mts +2 -2
  101. package/src/rules/prefer-num-safe-parse-float.test.mts +255 -94
  102. package/src/rules/prefer-num-safe-parse-int.test.mts +212 -76
  103. package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
  104. package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
  105. package/src/rules/prefer-pipe-map-nullable.mts +17 -19
  106. package/src/rules/prefer-range-for-loop.mts +37 -61
  107. package/src/rules/prefer-range-for-loop.test.mts +2 -2
@@ -406,13 +406,73 @@ describe('no-unnecessary-type-guard', () => {
406
406
  const y = isNonNullish(x);
407
407
  `,
408
408
  output: dedent`
409
- import { isNotNull } from 'ts-data-forge';
410
- import { isNonNullish } from 'ts-data-forge';
409
+ import { isNonNullish, isNotNull } from 'ts-data-forge';
411
410
  declare const x: string | null;
412
411
  const y = isNotNull(x);
413
412
  `,
414
413
  errors: [{ messageId: 'replaceTypeGuard' }],
415
414
  },
415
+ {
416
+ name: 'the message names an aliased replacement guard by its local name',
417
+ code: dedent`
418
+ import { isNonNullish, isNotNull as inn } from 'ts-data-forge';
419
+ declare const x: string | null;
420
+ const y = isNonNullish(x);
421
+ `,
422
+ output: dedent`
423
+ import { isNonNullish, isNotNull as inn } from 'ts-data-forge';
424
+ declare const x: string | null;
425
+ const y = inn(x);
426
+ `,
427
+ errors: [
428
+ {
429
+ messageId: 'replaceTypeGuard',
430
+ data: { from: 'isNonNullish', to: 'inn' },
431
+ },
432
+ ],
433
+ },
434
+ {
435
+ name: 'several reports bind each replacement guard they write',
436
+ code: dedent`
437
+ import { isNonNullish } from 'ts-data-forge';
438
+ declare const x: string | null;
439
+ declare const z: string | undefined;
440
+ const a = isNonNullish(x);
441
+ const b = isNonNullish(x);
442
+ const c = isNonNullish(z);
443
+ `,
444
+ output: [
445
+ dedent`
446
+ import { isNonNullish, isNotNull } from 'ts-data-forge';
447
+ declare const x: string | null;
448
+ declare const z: string | undefined;
449
+ const a = isNotNull(x);
450
+ const b = isNonNullish(x);
451
+ const c = isNonNullish(z);
452
+ `,
453
+ dedent`
454
+ import { isNonNullish, isNotNull, isNotUndefined } from 'ts-data-forge';
455
+ declare const x: string | null;
456
+ declare const z: string | undefined;
457
+ const a = isNotNull(x);
458
+ const b = isNonNullish(x);
459
+ const c = isNotUndefined(z);
460
+ `,
461
+ dedent`
462
+ import { isNonNullish, isNotNull, isNotUndefined } from 'ts-data-forge';
463
+ declare const x: string | null;
464
+ declare const z: string | undefined;
465
+ const a = isNotNull(x);
466
+ const b = isNotNull(x);
467
+ const c = isNotUndefined(z);
468
+ `,
469
+ ],
470
+ errors: [
471
+ { messageId: 'replaceTypeGuard' },
472
+ { messageId: 'replaceTypeGuard' },
473
+ { messageId: 'replaceTypeGuard' },
474
+ ],
475
+ },
416
476
  {
417
477
  name: 'isNullish that only matches undefined -> isUndefined',
418
478
  code: dedent`
@@ -6,11 +6,8 @@ import {
6
6
  type TSESTree,
7
7
  } from '@typescript-eslint/utils';
8
8
  import {
9
- buildNamedImportFix,
10
9
  createGetLayerTypes,
11
10
  getArgumentText,
12
- getImportDeclarations,
13
- getImportedLocalName,
14
11
  negationOf,
15
12
  prefixedNegationOf,
16
13
  skipTypeWrappers,
@@ -19,7 +16,7 @@ import {
19
16
  import { Arr, hasKey, isRecord } from 'ts-data-forge';
20
17
  import type { FixedLengthTuple } from 'ts-type-forge';
21
18
  import * as ts from 'typescript';
22
- import { TS_DATA_FORGE_MODULE } from './constants.mjs';
19
+ import { tsDataForgeValuesAt } from './import-binding.mjs';
23
20
  import { needsMutableResult } from './mutable-result.mjs';
24
21
 
25
22
  type Options = readonly [];
@@ -34,8 +31,8 @@ type Match = Readonly<{
34
31
  }>;
35
32
 
36
33
  /**
37
- * The callback of `xs.flatMap((x) => (cond ? [a, b] : []))` decides per
38
- * element whether to keep it and what to keep. Written as a loop, that is
34
+ * The callback of `xs.flatMap((x) => (cond ? [a, b] : []))` decides per element
35
+ * whether to keep it and what to keep. Written as a loop, that is
39
36
  *
40
37
  * ```ts
41
38
  * Arr.generate(function* () {
@@ -106,13 +103,6 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
106
103
  create: (context) => {
107
104
  const sourceCode = context.sourceCode;
108
105
 
109
- const program = sourceCode.ast;
110
-
111
- const tsDataForgeImports = getImportDeclarations(
112
- program,
113
- TS_DATA_FORGE_MODULE,
114
- );
115
-
116
106
  const services = sourceCode.parserServices;
117
107
 
118
108
  const tsProgram = services?.program ?? undefined;
@@ -132,8 +122,8 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
132
122
  esTreeNodeToTSNodeMap,
133
123
  });
134
124
 
135
- // The negation is the test of the loop's `if`, where only its truthiness
136
- // is read, so `!!a` may always be written `a`.
125
+ // The negation is the test of the loop's `if`, where only its truthiness is
126
+ // read, so `!!a` may always be written `a`.
137
127
  const negationContext: NegationContext = {
138
128
  sourceCode,
139
129
  checker,
@@ -146,9 +136,10 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
146
136
  node: TSESTree.Node,
147
137
  ): ts.Node | undefined => esTreeNodeToTSNodeMap.get(node);
148
138
 
149
- const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
139
+ // The name the rewrite writes, and, at a node, the edits that bind it.
140
+ const arr = tsDataForgeValuesAt(sourceCode, ['Arr']);
150
141
 
151
- const arrName = arrLocalName ?? 'Arr';
142
+ const arrName = arr.localNames[0] ?? 'Arr';
152
143
 
153
144
  const mut_matches: Match[] = [];
154
145
 
@@ -172,8 +163,8 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
172
163
 
173
164
  const callback = node.arguments[0];
174
165
 
175
- // A wrapper around the callback may be what types its parameter, so
176
- // a wrapped callback is not unwrapped.
166
+ // A wrapper around the callback may be what types its parameter, so a
167
+ // wrapped callback is not unwrapped.
177
168
  if (
178
169
  callback.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
179
170
  callback.async ||
@@ -291,45 +282,53 @@ export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
291
282
  ),
292
283
  true,
293
284
  ) &&
294
- !losesComments(sourceCode, callback, shape) &&
285
+ !losesComments(
286
+ sourceCode,
287
+ node,
288
+ [
289
+ receiver.range,
290
+ param.range,
291
+ ...(typeArguments === undefined ? [] : [typeArguments.range]),
292
+ ],
293
+ shape,
294
+ ) &&
295
295
  !needsMutableResult(sourceCode, checker, getTsNode, node);
296
296
 
297
297
  mut_matches.push({ node, replacement, autofixable });
298
298
  },
299
299
  'Program:exit': () => {
300
- // The import is added by the first fix only, so that several fixes do
301
- // not insert it at the same position. When the first match is not
302
- // autofixable, the first autofixable one carries it instead.
303
- const firstAutofixable = mut_matches.find((m) => m.autofixable);
304
-
300
+ // Every fix brings `Arr` into scope itself, so that one applied alone
301
+ // (an editor's quick fix) is complete.
305
302
  for (const match of mut_matches) {
306
- const buildFix =
307
- (withImport: boolean): TSESLint.ReportFixFunction =>
308
- (fixer) => [
309
- ...(withImport && arrLocalName === undefined
310
- ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
311
- 'Arr',
312
- ])
313
- : []),
314
- fixer.replaceText(match.node, match.replacement),
315
- ];
303
+ const binding = arr.at(match.node);
304
+
305
+ const fix: TSESLint.ReportFixFunction | null =
306
+ binding === undefined
307
+ ? null
308
+ : (fixer) => [
309
+ ...binding.edits(fixer),
310
+ fixer.replaceText(match.node, match.replacement),
311
+ ];
316
312
 
317
313
  if (match.autofixable) {
318
314
  context.report({
319
315
  node: match.node,
320
316
  messageId: 'useArrGenerate',
321
- fix: buildFix(match === firstAutofixable),
317
+ fix,
322
318
  });
323
319
  } else {
324
320
  context.report({
325
321
  node: match.node,
326
322
  messageId: 'useArrGenerate',
327
- suggest: [
328
- {
329
- messageId: 'suggestArrGenerate',
330
- fix: buildFix(true),
331
- },
332
- ],
323
+ suggest:
324
+ fix === null
325
+ ? []
326
+ : [
327
+ {
328
+ messageId: 'suggestArrGenerate',
329
+ fix,
330
+ },
331
+ ],
333
332
  });
334
333
  }
335
334
  }
@@ -452,6 +451,63 @@ const getFilterMapShape = (
452
451
  };
453
452
  };
454
453
 
454
+ /** The whitespace at the start of the line that `index` is on. */
455
+ const lineIndentOf = (
456
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
457
+ sourceCode: TSESLint.SourceCode,
458
+ index: number,
459
+ ): string => {
460
+ const { line } = sourceCode.getLocFromIndex(index);
461
+
462
+ const lineText = sourceCode.lines[line - 1] ?? '';
463
+
464
+ return /^\s*/u.exec(lineText)?.[0] ?? '';
465
+ };
466
+
467
+ /**
468
+ * `text`, the source at `range`, with every line after the first shifted from
469
+ * the indentation of the line `range` starts on to `targetIndent`. Left as it
470
+ * is when a template literal or string spans lines inside it, since shifting
471
+ * those lines would change the string.
472
+ */
473
+ const reindent = (
474
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
475
+ sourceCode: TSESLint.SourceCode,
476
+ range: FixedLengthTuple<2, number>,
477
+ targetIndent: string,
478
+ text: string,
479
+ ): string => {
480
+ if (!text.includes('\n')) {
481
+ return text;
482
+ }
483
+
484
+ const spansLines = sourceCode.ast.tokens.some(
485
+ (token) =>
486
+ range[0] <= token.range[0] &&
487
+ token.range[1] <= range[1] &&
488
+ (token.type === AST_TOKEN_TYPES.Template ||
489
+ token.type === AST_TOKEN_TYPES.String) &&
490
+ token.loc.start.line !== token.loc.end.line,
491
+ );
492
+
493
+ if (spansLines) {
494
+ return text;
495
+ }
496
+
497
+ const sourceIndent = lineIndentOf(sourceCode, range[0]);
498
+
499
+ const [first, ...rest] = text.split('\n');
500
+
501
+ return [
502
+ first,
503
+ ...rest.map((line) =>
504
+ line.startsWith(sourceIndent)
505
+ ? `${targetIndent}${line.slice(sourceIndent.length)}`
506
+ : line,
507
+ ),
508
+ ].join('\n');
509
+ };
510
+
455
511
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
456
512
  const buildReplacement = ({
457
513
  sourceCode,
@@ -534,19 +590,29 @@ const buildReplacement = ({
534
590
  };
535
591
 
536
592
  /**
537
- * Whether a comment inside `callback` lies outside every piece of source the
538
- * rewrite carries over, and would therefore be dropped.
593
+ * Whether a comment inside `node`, the call the fix replaces, lies outside
594
+ * every range the rewrite copies verbatim, and would therefore be dropped.
595
+ *
596
+ * `copied` lists those ranges besides the shape's: the receiver, the type
597
+ * arguments and the parameter. The test counts only when it is copied as it is:
598
+ * a negated test is rebuilt by `negationOf`, which can drop a comment (one
599
+ * between the operands of `a && b` is gone from `!a || !b`), so any comment in
600
+ * one keeps the fix a suggestion, even where the negation carries it over. A
601
+ * comment between the receiver and `.flatMap`, after `flatMap(` or after the
602
+ * callback is in none of the ranges.
539
603
  */
540
604
  const losesComments = (
541
605
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
542
606
  sourceCode: TSESLint.SourceCode,
543
607
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
544
- callback: TSESTree.ArrowFunctionExpression,
608
+ node: TSESTree.CallExpression,
609
+ copied: readonly FixedLengthTuple<2, number>[],
545
610
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
546
611
  shape: FilterMapShape,
547
612
  ): boolean => {
548
613
  const keptRanges: readonly FixedLengthTuple<2, number>[] = [
549
- shape.test.range,
614
+ ...copied,
615
+ ...(shape.negated ? ([] as const) : ([shape.test.range] as const)),
550
616
  ...shape.elements.map((element) => element.range),
551
617
  ...(shape.preludeRange === undefined
552
618
  ? ([] as const)
@@ -554,7 +620,7 @@ const losesComments = (
554
620
  ] as const;
555
621
 
556
622
  return sourceCode
557
- .getCommentsInside(callback)
623
+ .getCommentsInside(node)
558
624
  .some((comment) =>
559
625
  keptRanges.every(
560
626
  ([start, end]) =>
@@ -563,63 +629,6 @@ const losesComments = (
563
629
  );
564
630
  };
565
631
 
566
- /**
567
- * `text`, the source at `range`, with every line after the first shifted from
568
- * the indentation of the line `range` starts on to `targetIndent`. Left as it
569
- * is when a template literal or string spans lines inside it, since shifting
570
- * those lines would change the string.
571
- */
572
- const reindent = (
573
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
574
- sourceCode: TSESLint.SourceCode,
575
- range: FixedLengthTuple<2, number>,
576
- targetIndent: string,
577
- text: string,
578
- ): string => {
579
- if (!text.includes('\n')) {
580
- return text;
581
- }
582
-
583
- const spansLines = sourceCode.ast.tokens.some(
584
- (token) =>
585
- range[0] <= token.range[0] &&
586
- token.range[1] <= range[1] &&
587
- (token.type === AST_TOKEN_TYPES.Template ||
588
- token.type === AST_TOKEN_TYPES.String) &&
589
- token.loc.start.line !== token.loc.end.line,
590
- );
591
-
592
- if (spansLines) {
593
- return text;
594
- }
595
-
596
- const sourceIndent = lineIndentOf(sourceCode, range[0]);
597
-
598
- const [first, ...rest] = text.split('\n');
599
-
600
- return [
601
- first,
602
- ...rest.map((line) =>
603
- line.startsWith(sourceIndent)
604
- ? `${targetIndent}${line.slice(sourceIndent.length)}`
605
- : line,
606
- ),
607
- ].join('\n');
608
- };
609
-
610
- /** The whitespace at the start of the line that `index` is on. */
611
- const lineIndentOf = (
612
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
613
- sourceCode: TSESLint.SourceCode,
614
- index: number,
615
- ): string => {
616
- const { line } = sourceCode.getLocFromIndex(index);
617
-
618
- const lineText = sourceCode.lines[line - 1] ?? '';
619
-
620
- return /^\s*/u.exec(lineText)?.[0] ?? '';
621
- };
622
-
623
632
  /** Whether `node`, with its type wrappers taken off, is a `….filter(…)` call. */
624
633
  const isFilterCall = (
625
634
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -642,78 +651,30 @@ const isFilterCall = (
642
651
  };
643
652
 
644
653
  /**
645
- * Whether `node`, a reference or a member access, has a type here narrower
646
- * than its declared one, by a narrowing that a function created here would not
647
- * see. TypeScript keeps the narrowing of a `const`, and of a parameter or `let`
648
- * that is never reassigned, inside a closure; it drops every other one,
649
- * including any narrowing of a property.
654
+ * Whether `type` is an array or tuple, or a union of them, or an intersection
655
+ * with one in it (`readonly T[] & Brand`).
650
656
  */
651
- const isNarrowedHere = (
652
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
653
- sourceCode: TSESLint.SourceCode,
657
+ const isArrayOrTupleType = (
654
658
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
655
659
  checker: ts.TypeChecker,
656
660
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
657
- getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
658
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
659
- node: TSESTree.Node,
660
- isReceiver: boolean,
661
+ type: ts.Type,
661
662
  ): boolean => {
662
- if (
663
- node.type !== AST_NODE_TYPES.Identifier &&
664
- node.type !== AST_NODE_TYPES.MemberExpression
665
- ) {
666
- return false;
667
- }
668
-
669
- // A method about to be called (`xs.flat()`): its type is that of the method,
670
- // which no narrowing of interest changes.
671
- if (
672
- node.parent.type === AST_NODE_TYPES.CallExpression &&
673
- node.parent.callee === node
674
- ) {
675
- return false;
676
- }
677
-
678
- if (node.type === AST_NODE_TYPES.Identifier) {
679
- if (!isReference(node)) {
680
- return false;
681
- }
682
-
683
- const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
684
-
685
- if (
686
- variable?.references.every(
687
- (reference) => !reference.isWrite() || reference.init === true,
688
- ) === true
689
- ) {
690
- return false;
691
- }
692
- }
693
-
694
- const tsNode = getTsNode(node);
695
-
696
- if (tsNode === undefined) {
697
- return false;
698
- }
699
-
700
- const declaredType = getDeclaredType(checker, tsNode);
701
-
702
- const type = checker.getTypeAtLocation(tsNode);
663
+ const resolved =
664
+ (type.flags & ts.TypeFlags.TypeParameter) !== 0
665
+ ? (checker.getBaseConstraintOfType(type) ?? type)
666
+ : type;
703
667
 
704
- return (
705
- declaredType !== undefined &&
706
- declaredType !== type &&
707
- // The loop only iterates the receiver itself, so a narrowing that leaves
708
- // it an array of the same elements (`Arr.isNonEmpty(xs)`) loses nothing.
709
- !(isReceiver && iteratesTheSame(checker, declaredType, type))
710
- );
668
+ return resolved.isUnion()
669
+ ? resolved.types.every((t) => isArrayOrTupleType(checker, t))
670
+ : resolved.isIntersection()
671
+ ? resolved.types.some((t) => isArrayOrTupleType(checker, t))
672
+ : checker.isArrayType(resolved) || checker.isTupleType(resolved);
711
673
  };
712
674
 
713
675
  /**
714
- * Whether two types are both arrays or tuples whose elements have one type,
715
- * as the declared and the narrowed type of `xs` after `Arr.isNonEmpty(xs)`
716
- * are.
676
+ * Whether two types are both arrays or tuples whose elements have one type, as
677
+ * the declared and the narrowed type of `xs` after `Arr.isNonEmpty(xs)` are.
717
678
  */
718
679
  const iteratesTheSame = (
719
680
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -740,8 +701,8 @@ const iteratesTheSame = (
740
701
  * `undefined` when there is none to compare with (an index read with no
741
702
  * property behind it, which nothing narrows).
742
703
  *
743
- * A property's is read off the type of the object it is accessed on, which
744
- * also covers a property of a mapped type (`Readonly<…>`), whose symbol has no
704
+ * A property's is read off the type of the object it is accessed on, which also
705
+ * covers a property of a mapped type (`Readonly<…>`), whose symbol has no
745
706
  * declaration of its own. A variable's is the type at its declaration
746
707
  * (`checker.getTypeOfSymbol` would say the same, but is missing before
747
708
  * TypeScript 5.1).
@@ -787,10 +748,97 @@ const getDeclaredType = (
787
748
  : checker.getTypeOfSymbolAtLocation(symbol, declaration);
788
749
  };
789
750
 
751
+ /** Whether an identifier reads a binding, rather than naming a property or a key. */
752
+ const isReference = (
753
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
754
+ node: TSESTree.Identifier,
755
+ ): boolean => {
756
+ const { parent } = node;
757
+
758
+ return !(
759
+ (parent.type === AST_NODE_TYPES.MemberExpression &&
760
+ parent.property === node &&
761
+ !parent.computed) ||
762
+ (parent.type === AST_NODE_TYPES.Property &&
763
+ parent.key === node &&
764
+ !parent.computed &&
765
+ !parent.shorthand)
766
+ );
767
+ };
768
+
790
769
  /**
791
- * Whether the binding `identifier` reads is declared inside `container`, as
792
- * the parameter of an arrow function in the receiver is: the loop variable
793
- * does not shadow it.
770
+ * Whether `node`, a reference or a member access, has a type here narrower than
771
+ * its declared one, by a narrowing that a function created here would not see.
772
+ * TypeScript keeps the narrowing of a `const`, and of a parameter or `let` that
773
+ * is never reassigned, inside a closure; it drops every other one, including
774
+ * any narrowing of a property.
775
+ */
776
+ const isNarrowedHere = (
777
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
778
+ sourceCode: TSESLint.SourceCode,
779
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
780
+ checker: ts.TypeChecker,
781
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
782
+ getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
783
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
784
+ node: TSESTree.Node,
785
+ isReceiver: boolean,
786
+ ): boolean => {
787
+ if (
788
+ node.type !== AST_NODE_TYPES.Identifier &&
789
+ node.type !== AST_NODE_TYPES.MemberExpression
790
+ ) {
791
+ return false;
792
+ }
793
+
794
+ // A method about to be called (`xs.flat()`): its type is that of the method,
795
+ // which no narrowing of interest changes.
796
+ if (
797
+ node.parent.type === AST_NODE_TYPES.CallExpression &&
798
+ node.parent.callee === node
799
+ ) {
800
+ return false;
801
+ }
802
+
803
+ if (node.type === AST_NODE_TYPES.Identifier) {
804
+ if (!isReference(node)) {
805
+ return false;
806
+ }
807
+
808
+ const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
809
+
810
+ if (
811
+ variable?.references.every(
812
+ (reference) => !reference.isWrite() || reference.init === true,
813
+ ) === true
814
+ ) {
815
+ return false;
816
+ }
817
+ }
818
+
819
+ const tsNode = getTsNode(node);
820
+
821
+ if (tsNode === undefined) {
822
+ return false;
823
+ }
824
+
825
+ const declaredType = getDeclaredType(checker, tsNode);
826
+
827
+ const type = checker.getTypeAtLocation(tsNode);
828
+
829
+ return (
830
+ declaredType !== undefined &&
831
+ declaredType !== type &&
832
+ // The loop only iterates the receiver itself, so a narrowing that leaves it
833
+ // an array of the same elements (`Arr.isNonEmpty(xs)`) loses nothing.
834
+ !(isReceiver && iteratesTheSame(checker, declaredType, type))
835
+ );
836
+ };
837
+
838
+ /**
839
+ * Whether the binding `identifier` reads is declared inside `container`, as the
840
+ * parameter of an arrow function in the receiver is: the loop variable does not
841
+ * shadow it.
794
842
  */
795
843
  const isBoundWithin = (
796
844
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -816,23 +864,8 @@ const isBoundWithin = (
816
864
  );
817
865
  };
818
866
 
819
- /** Whether an identifier reads a binding, rather than naming a property or a key. */
820
- const isReference = (
821
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
822
- node: TSESTree.Identifier,
823
- ): boolean => {
824
- const { parent } = node;
825
-
826
- return !(
827
- (parent.type === AST_NODE_TYPES.MemberExpression &&
828
- parent.property === node &&
829
- !parent.computed) ||
830
- (parent.type === AST_NODE_TYPES.Property &&
831
- parent.key === node &&
832
- !parent.computed &&
833
- !parent.shorthand)
834
- );
835
- };
867
+ const isNode = (value: unknown): value is TSESTree.Node =>
868
+ isRecord(value) && hasKey(value, 'type') && typeof value.type === 'string';
836
869
 
837
870
  /**
838
871
  * Whether `node` or a node inside it satisfies `predicate`. A nested
@@ -877,9 +910,6 @@ const someDescendant = (
877
910
  });
878
911
  };
879
912
 
880
- const isNode = (value: unknown): value is TSESTree.Node =>
881
- isRecord(value) && hasKey(value, 'type') && typeof value.type === 'string';
882
-
883
913
  const isReturnStatement = (
884
914
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
885
915
  node: TSESTree.Node,
@@ -894,25 +924,3 @@ const rebindsInGenerator = (
894
924
  node.type === AST_NODE_TYPES.Super ||
895
925
  node.type === AST_NODE_TYPES.MetaProperty ||
896
926
  (node.type === AST_NODE_TYPES.Identifier && node.name === 'arguments');
897
-
898
- /**
899
- * Whether `type` is an array or tuple, or a union of them, or an intersection
900
- * with one in it (`readonly T[] & Brand`).
901
- */
902
- const isArrayOrTupleType = (
903
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
904
- checker: ts.TypeChecker,
905
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
906
- type: ts.Type,
907
- ): boolean => {
908
- const resolved =
909
- (type.flags & ts.TypeFlags.TypeParameter) !== 0
910
- ? (checker.getBaseConstraintOfType(type) ?? type)
911
- : type;
912
-
913
- return resolved.isUnion()
914
- ? resolved.types.every((t) => isArrayOrTupleType(checker, t))
915
- : resolved.isIntersection()
916
- ? resolved.types.some((t) => isArrayOrTupleType(checker, t))
917
- : checker.isArrayType(resolved) || checker.isTupleType(resolved);
918
- };