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

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 (88) hide show
  1. package/dist/rules/ast-utils.d.mts +5 -5
  2. package/dist/rules/ast-utils.d.mts.map +1 -1
  3. package/dist/rules/ast-utils.mjs +3 -3
  4. package/dist/rules/ast-utils.mjs.map +1 -1
  5. package/dist/rules/import-binding.d.mts +38 -0
  6. package/dist/rules/import-binding.d.mts.map +1 -0
  7. package/dist/rules/import-binding.mjs +53 -0
  8. package/dist/rules/import-binding.mjs.map +1 -0
  9. package/dist/rules/mutable-result.mjs +37 -37
  10. package/dist/rules/mutable-result.mjs.map +1 -1
  11. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  12. package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
  13. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  14. package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
  15. package/dist/rules/prefer-arr-generate.mjs +129 -131
  16. package/dist/rules/prefer-arr-generate.mjs.map +1 -1
  17. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  18. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
  19. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  21. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +15 -17
  22. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  23. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  24. package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
  25. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  26. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  27. package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
  28. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  29. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
  31. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  33. package/dist/rules/prefer-arr-scan.mjs +9 -18
  34. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  35. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  36. package/dist/rules/prefer-arr-sum.mjs +16 -21
  37. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  38. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  39. package/dist/rules/prefer-arr-uniq.mjs +38 -43
  40. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  41. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  42. package/dist/rules/prefer-as-int.mjs +19 -26
  43. package/dist/rules/prefer-as-int.mjs.map +1 -1
  44. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  45. package/dist/rules/prefer-canonical-array-slicing.mjs +49 -15
  46. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  47. package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
  48. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  49. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  50. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
  51. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  52. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
  53. package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
  54. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
  55. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  56. package/dist/rules/prefer-range-for-loop.mjs +18 -24
  57. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  58. package/package.json +4 -4
  59. package/src/rules/ast-utils.mts +11 -11
  60. package/src/rules/import-binding.mts +94 -0
  61. package/src/rules/mutable-result.mts +56 -56
  62. package/src/rules/no-unnecessary-type-guard.mts +199 -203
  63. package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
  64. package/src/rules/prefer-arr-generate.mts +184 -194
  65. package/src/rules/prefer-arr-generate.test.mts +262 -134
  66. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
  67. package/src/rules/prefer-arr-is-fixed-length-array.mts +24 -36
  68. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
  69. package/src/rules/prefer-arr-is-max-length-array.mts +21 -34
  70. package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
  71. package/src/rules/prefer-arr-is-min-length-array.mts +21 -34
  72. package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
  73. package/src/rules/prefer-arr-is-non-empty.mts +17 -34
  74. package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
  75. package/src/rules/prefer-arr-scan.mts +11 -36
  76. package/src/rules/prefer-arr-scan.test.mts +55 -0
  77. package/src/rules/prefer-arr-sum.mts +19 -33
  78. package/src/rules/prefer-arr-uniq.mts +49 -60
  79. package/src/rules/prefer-arr-uniq.test.mts +46 -20
  80. package/src/rules/prefer-as-int.mts +27 -52
  81. package/src/rules/prefer-as-int.test.mts +79 -29
  82. package/src/rules/prefer-canonical-array-slicing.mts +86 -24
  83. package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
  84. package/src/rules/prefer-canonical-length-cast.mts +18 -18
  85. package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
  86. package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
  87. package/src/rules/prefer-pipe-map-nullable.mts +6 -8
  88. package/src/rules/prefer-range-for-loop.mts +31 -55
@@ -5,11 +5,14 @@ import {
5
5
  type TSESTree,
6
6
  } from '@typescript-eslint/utils';
7
7
  import {
8
- buildNamedImportFix,
8
+ bindValueImports,
9
9
  getImportDeclarations,
10
10
  getImportedLocalName,
11
+ hasForeignBinding,
12
+ importedNameOf,
11
13
  skipTypeWrappers,
12
14
  typeWrapperLayers,
15
+ type ValueImportBinding,
13
16
  } from 'eslint-rule-forge';
14
17
  import type { ReadonlyRecord } from 'ts-type-forge';
15
18
  import * as ts from 'typescript';
@@ -210,10 +213,16 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
210
213
  const checker = parserServices.program.getTypeChecker();
211
214
 
212
215
  /**
213
- * The in-scope way to reference the `Arr` namespace: a named import
214
- * (possibly aliased), a namespace import member, or the canonical `Arr`
215
- * (added by the import fix when nothing is imported yet).
216
+ * The in-scope way to reference the `Arr` namespace, the name the
217
+ * reference starts with, and the edits that bring it into scope: a value
218
+ * import of `Arr` (possibly aliased), a namespace import member, or `Arr`
219
+ * bound by {@link bindValueImports}, which adds it to an existing import or
220
+ * drops `type` from one. `undefined` when none can: `import type { Arr }`
221
+ * with no namespace import.
216
222
  */
223
+ const arrTarget = resolveArrTarget(sourceCode, tsDataForgeImports);
224
+
225
+ /** How a call already in the file names `Arr`, to recognize it. */
217
226
  const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
218
227
 
219
228
  const namespaceLocalName = tsDataForgeImports
@@ -223,12 +232,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
223
232
  specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
224
233
  )?.local.name;
225
234
 
226
- const arrRef =
227
- arrLocalName ??
228
- (namespaceLocalName === undefined ? 'Arr' : `${namespaceLocalName}.Arr`);
229
-
230
- const needsArrImport =
231
- arrLocalName === undefined && namespaceLocalName === undefined;
235
+ const arrRef = arrTarget?.ref ?? 'Arr';
232
236
 
233
237
  /**
234
238
  * Deferred (non-concrete) types are reduced to their base constraint
@@ -855,27 +859,85 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
855
859
  },
856
860
 
857
861
  'Program:exit': () => {
858
- for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
862
+ // Every fix brings `Arr` into scope itself, so that one applied alone
863
+ // (an editor's quick fix) is complete.
864
+ for (const nodeInfo of mut_nodesToFix) {
865
+ const writable =
866
+ arrTarget !== undefined &&
867
+ !hasForeignBinding(
868
+ sourceCode,
869
+ nodeInfo.node,
870
+ arrTarget.boundName,
871
+ TS_DATA_FORGE_MODULE,
872
+ );
873
+
859
874
  context.report({
860
875
  node: nodeInfo.node,
861
876
  messageId: 'preferCanonicalArraySlicing',
862
877
  data: { replacement: nodeInfo.replacement },
863
- fix: (fixer) => {
864
- const importFixes =
865
- index === 0 && needsArrImport
866
- ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
867
- 'Arr',
868
- ])
869
- : [];
870
-
871
- return [
872
- ...importFixes,
873
- fixer.replaceText(nodeInfo.node, nodeInfo.replacement),
874
- ];
875
- },
878
+ fix: writable
879
+ ? (fixer) => [
880
+ ...arrTarget.edits(fixer),
881
+ fixer.replaceText(nodeInfo.node, nodeInfo.replacement),
882
+ ]
883
+ : null,
876
884
  });
877
885
  }
878
886
  },
879
887
  };
880
888
  },
881
889
  } as const;
890
+
891
+ /**
892
+ * How `Arr` is written in this file; see where it is called. A value import of
893
+ * `Arr` comes first, then a namespace import, and only then an edit.
894
+ */
895
+ const resolveArrTarget = (
896
+ sourceCode: TSESLint.SourceCode,
897
+ tsDataForgeImports: readonly TSESTree.ImportDeclaration[],
898
+ ):
899
+ | Readonly<{
900
+ ref: string;
901
+ boundName: string;
902
+ edits: ValueImportBinding['edits'];
903
+ }>
904
+ | undefined => {
905
+ const valueSpecifiers = tsDataForgeImports
906
+ .filter((declaration) => declaration.importKind !== 'type')
907
+ .flatMap((declaration) => declaration.specifiers);
908
+
909
+ const arrImport = valueSpecifiers.find(
910
+ (specifier) =>
911
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
912
+ specifier.importKind !== 'type' &&
913
+ importedNameOf(specifier) === 'Arr',
914
+ );
915
+
916
+ if (arrImport !== undefined) {
917
+ return {
918
+ ref: arrImport.local.name,
919
+ boundName: arrImport.local.name,
920
+ edits: () => [],
921
+ };
922
+ }
923
+
924
+ const namespace = valueSpecifiers.find(
925
+ (specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,
926
+ );
927
+
928
+ if (namespace !== undefined) {
929
+ return {
930
+ ref: `${namespace.local.name}.Arr`,
931
+ boundName: namespace.local.name,
932
+ edits: () => [],
933
+ };
934
+ }
935
+
936
+ const binding = bindValueImports(sourceCode, TS_DATA_FORGE_MODULE, ['Arr']);
937
+
938
+ const localName = binding?.localNames[0];
939
+
940
+ return binding === undefined || localName === undefined
941
+ ? undefined
942
+ : { ref: localName, boundName: localName, edits: binding.edits };
943
+ };
@@ -572,12 +572,20 @@ describe('prefer-canonical-array-slicing', () => {
572
572
  const ys = xs.slice(1);
573
573
  const zs = xs.slice(0, -1);
574
574
  `,
575
- output: dedent`
576
- import { Arr } from 'ts-data-forge';
577
- declare const xs: readonly number[];
578
- const ys = Arr.tail(xs);
579
- const zs = Arr.butLast(xs);
580
- `,
575
+ output: [
576
+ dedent`
577
+ import { Arr } from 'ts-data-forge';
578
+ declare const xs: readonly number[];
579
+ const ys = Arr.tail(xs);
580
+ const zs = xs.slice(0, -1);
581
+ `,
582
+ dedent`
583
+ import { Arr } from 'ts-data-forge';
584
+ declare const xs: readonly number[];
585
+ const ys = Arr.tail(xs);
586
+ const zs = Arr.butLast(xs);
587
+ `,
588
+ ],
581
589
  errors: [
582
590
  { messageId: 'preferCanonicalArraySlicing' },
583
591
  { messageId: 'preferCanonicalArraySlicing' },
@@ -629,13 +637,22 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
629
637
  const b = xs.slice(0 satisfies number, -1 as number);
630
638
  const c = xs.toSpliced(2 satisfies number);
631
639
  `,
632
- output: dedent`
633
- import { Arr } from 'ts-data-forge';
634
- declare const xs: readonly number[];
635
- const a = Arr.tail(xs);
636
- const b = Arr.butLast(xs);
637
- const c = Arr.take(xs, 2);
638
- `,
640
+ output: [
641
+ dedent`
642
+ import { Arr } from 'ts-data-forge';
643
+ declare const xs: readonly number[];
644
+ const a = Arr.tail(xs);
645
+ const b = xs.slice(0 satisfies number, -1 as number);
646
+ const c = xs.toSpliced(2 satisfies number);
647
+ `,
648
+ dedent`
649
+ import { Arr } from 'ts-data-forge';
650
+ declare const xs: readonly number[];
651
+ const a = Arr.tail(xs);
652
+ const b = Arr.butLast(xs);
653
+ const c = Arr.take(xs, 2);
654
+ `,
655
+ ],
639
656
  errors: [
640
657
  { messageId: 'preferCanonicalArraySlicing' },
641
658
  { messageId: 'preferCanonicalArraySlicing' },
@@ -663,13 +680,22 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
663
680
  const ys = (xs satisfies readonly number[]).slice(1);
664
681
  const zs = [...xs!, v!];
665
682
  `,
666
- output: dedent`
667
- import { Arr } from 'ts-data-forge';
668
- declare const xs: readonly number[];
669
- declare const v: number | undefined;
670
- const ys = Arr.tail(xs satisfies readonly number[]);
671
- const zs = Arr.toPushed(xs!, v!);
672
- `,
683
+ output: [
684
+ dedent`
685
+ import { Arr } from 'ts-data-forge';
686
+ declare const xs: readonly number[];
687
+ declare const v: number | undefined;
688
+ const ys = Arr.tail(xs satisfies readonly number[]);
689
+ const zs = [...xs!, v!];
690
+ `,
691
+ dedent`
692
+ import { Arr } from 'ts-data-forge';
693
+ declare const xs: readonly number[];
694
+ declare const v: number | undefined;
695
+ const ys = Arr.tail(xs satisfies readonly number[]);
696
+ const zs = Arr.toPushed(xs!, v!);
697
+ `,
698
+ ],
673
699
  errors: [
674
700
  { messageId: 'preferCanonicalArraySlicing' },
675
701
  { messageId: 'preferCanonicalArraySlicing' },
@@ -697,14 +723,24 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
697
723
  const c = xs.filter((_, i) => i < (xs.length satisfies number) - 2);
698
724
  const d = xs.toSpliced(0, xs!.length - 2);
699
725
  `,
700
- output: dedent`
701
- import { Arr } from 'ts-data-forge';
702
- declare const xs: readonly number[];
703
- const a = Arr.butLast(xs);
704
- const b = Arr.butLast(xs);
705
- const c = Arr.skipLast(xs, 2);
706
- const d = Arr.takeLast(xs, 2);
707
- `,
726
+ output: [
727
+ dedent`
728
+ import { Arr } from 'ts-data-forge';
729
+ declare const xs: readonly number[];
730
+ const a = Arr.butLast(xs);
731
+ const b = xs.slice(0, (xs as readonly number[]).length - 1);
732
+ const c = xs.filter((_, i) => i < (xs.length satisfies number) - 2);
733
+ const d = xs.toSpliced(0, xs!.length - 2);
734
+ `,
735
+ dedent`
736
+ import { Arr } from 'ts-data-forge';
737
+ declare const xs: readonly number[];
738
+ const a = Arr.butLast(xs);
739
+ const b = Arr.butLast(xs);
740
+ const c = Arr.skipLast(xs, 2);
741
+ const d = Arr.takeLast(xs, 2);
742
+ `,
743
+ ],
708
744
  errors: [
709
745
  { messageId: 'preferCanonicalArraySlicing' },
710
746
  { messageId: 'preferCanonicalArraySlicing' },
@@ -719,12 +755,20 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
719
755
  const a = (xs satisfies readonly number[]).slice(0, xs.length - 1);
720
756
  const b = xs.toSpliced((xs.length as number), 0, 1);
721
757
  `,
722
- output: dedent`
723
- import { Arr } from 'ts-data-forge';
724
- declare const xs: readonly number[];
725
- const a = Arr.butLast(xs satisfies readonly number[]);
726
- const b = Arr.toPushed(xs, 1);
727
- `,
758
+ output: [
759
+ dedent`
760
+ import { Arr } from 'ts-data-forge';
761
+ declare const xs: readonly number[];
762
+ const a = Arr.butLast(xs satisfies readonly number[]);
763
+ const b = xs.toSpliced((xs.length as number), 0, 1);
764
+ `,
765
+ dedent`
766
+ import { Arr } from 'ts-data-forge';
767
+ declare const xs: readonly number[];
768
+ const a = Arr.butLast(xs satisfies readonly number[]);
769
+ const b = Arr.toPushed(xs, 1);
770
+ `,
771
+ ],
728
772
  errors: [
729
773
  { messageId: 'preferCanonicalArraySlicing' },
730
774
  { messageId: 'preferCanonicalArraySlicing' },
@@ -733,3 +777,58 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
733
777
  ],
734
778
  });
735
779
  });
780
+
781
+ describe('prefer-canonical-array-slicing and the import it adds', () => {
782
+ tester.run('prefer-canonical-array-slicing', preferCanonicalArraySlicing, {
783
+ valid: [],
784
+ invalid: [
785
+ {
786
+ name: 'writes through a namespace import, which needs no edit',
787
+ code: dedent`
788
+ import * as tdf from 'ts-data-forge';
789
+ declare const xs: readonly number[];
790
+ const ys = xs.slice(1);
791
+ `,
792
+ output: dedent`
793
+ import * as tdf from 'ts-data-forge';
794
+ declare const xs: readonly number[];
795
+ const ys = tdf.Arr.tail(xs);
796
+ `,
797
+ errors: [{ messageId: 'preferCanonicalArraySlicing' }],
798
+ },
799
+ {
800
+ name: 'joins an existing named import from the module',
801
+ code: dedent`
802
+ import { asInt } from 'ts-data-forge';
803
+ declare const xs: readonly number[];
804
+ const ys = xs.slice(1);
805
+ `,
806
+ output: dedent`
807
+ import { asInt, Arr } from 'ts-data-forge';
808
+ declare const xs: readonly number[];
809
+ const ys = Arr.tail(xs);
810
+ `,
811
+ errors: [{ messageId: 'preferCanonicalArraySlicing' }],
812
+ },
813
+ {
814
+ name: 'no fix when an import type binds the name',
815
+ code: dedent`
816
+ import type { Arr } from 'ts-data-forge';
817
+ declare const xs: readonly number[];
818
+ const ys = xs.slice(1);
819
+ `,
820
+ output: null,
821
+ errors: [{ messageId: 'preferCanonicalArraySlicing' }],
822
+ },
823
+ {
824
+ name: 'no fix where a local binding takes the name',
825
+ code: dedent`
826
+ declare const xs: readonly number[];
827
+ const f = (Arr: unknown) => xs.slice(1);
828
+ `,
829
+ output: null,
830
+ errors: [{ messageId: 'preferCanonicalArraySlicing' }],
831
+ },
832
+ ],
833
+ });
834
+ });
@@ -218,6 +218,24 @@ const getCastName = (
218
218
  : undefined;
219
219
  };
220
220
 
221
+ /**
222
+ * A non-negative integer literal's value, or `undefined` for anything else.
223
+ * Read through type wrappers: `1 as const` is still the bound `1`.
224
+ */
225
+ const getLengthLiteral = (
226
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
227
+ wrapped: TSESTree.Node,
228
+ ): number | undefined => {
229
+ const node = skipTypeWrappers(wrapped);
230
+
231
+ return node.type === AST_NODE_TYPES.Literal &&
232
+ typeof node.value === 'number' &&
233
+ Number.isSafeInteger(node.value) &&
234
+ node.value >= 0
235
+ ? node.value
236
+ : undefined;
237
+ };
238
+
221
239
  /**
222
240
  * Whether the call is `cast(...bounds, array)` with every bound written as the
223
241
  * exact numeric literal the rewrite requires.
@@ -319,21 +337,3 @@ const dropBoundsFix = (
319
337
  ? []
320
338
  : [fixer.removeRange([start.range[0], end.range[0]])];
321
339
  });
322
-
323
- /**
324
- * A non-negative integer literal's value, or `undefined` for anything else.
325
- * Read through type wrappers: `1 as const` is still the bound `1`.
326
- */
327
- const getLengthLiteral = (
328
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
329
- wrapped: TSESTree.Node,
330
- ): number | undefined => {
331
- const node = skipTypeWrappers(wrapped);
332
-
333
- return node.type === AST_NODE_TYPES.Literal &&
334
- typeof node.value === 'number' &&
335
- Number.isSafeInteger(node.value) &&
336
- node.value >= 0
337
- ? node.value
338
- : undefined;
339
- };