eslint-plugin-ts-data-forge 0.10.6 → 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 (56) 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 +3 -4
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +0 -1
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/import-binding.d.mts +6 -6
  12. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  13. package/dist/rules/prefer-arr-generate.d.mts +2 -2
  14. package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
  15. package/dist/rules/prefer-arr-generate.mjs +17 -5
  16. package/dist/rules/prefer-arr-generate.mjs.map +1 -1
  17. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  18. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  19. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  21. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  22. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  23. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  24. package/dist/rules/prefer-canonical-array-slicing.mjs +0 -1
  25. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  26. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  27. package/dist/rules/prefer-num-safe-parse-float.d.mts +1 -1
  28. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  29. package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -1
  30. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  31. package/dist/rules/prefer-pipe-map-nullable.d.mts +2 -2
  32. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
  33. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  34. package/dist/rules/rules.d.mts +2 -2
  35. package/package.json +6 -6
  36. package/src/plugin.mts +4 -3
  37. package/src/rule-types.mts +5 -5
  38. package/src/rules/ast-utils.mts +3 -4
  39. package/src/rules/import-binding.mts +6 -6
  40. package/src/rules/no-unnecessary-type-guard.mts +4 -4
  41. package/src/rules/prefer-arr-generate.mts +45 -27
  42. package/src/rules/prefer-arr-generate.test.mts +229 -0
  43. package/src/rules/prefer-arr-is-fixed-length-array.mts +2 -2
  44. package/src/rules/prefer-arr-is-max-length-array.mts +2 -2
  45. package/src/rules/prefer-arr-is-min-length-array.mts +2 -2
  46. package/src/rules/prefer-arr-scan.mts +8 -8
  47. package/src/rules/prefer-arr-sum.mts +3 -3
  48. package/src/rules/prefer-arr-uniq.mts +2 -2
  49. package/src/rules/prefer-canonical-array-slicing.mts +17 -18
  50. package/src/rules/prefer-canonical-length-cast.mts +11 -11
  51. package/src/rules/prefer-canonical-length-guard.mts +2 -2
  52. package/src/rules/prefer-num-safe-parse-float.test.mts +255 -94
  53. package/src/rules/prefer-num-safe-parse-int.test.mts +212 -76
  54. package/src/rules/prefer-pipe-map-nullable.mts +11 -11
  55. package/src/rules/prefer-range-for-loop.mts +6 -6
  56. package/src/rules/prefer-range-for-loop.test.mts +2 -2
@@ -884,6 +884,235 @@ describe('prefer-arr-generate', () => {
884
884
  { messageId: 'useArrGenerate' },
885
885
  ],
886
886
  },
887
+ {
888
+ name: 'only suggests when a comment between the receiver and `.flatMap` would be lost',
889
+ code: dedent`
890
+ declare const xs: readonly number[];
891
+ const ys = xs
892
+ // keep only the positives
893
+ .flatMap((x) => (x > 0 ? [x] : []));
894
+ `,
895
+ output: null,
896
+ errors: [
897
+ {
898
+ messageId: 'useArrGenerate',
899
+ suggestions: [
900
+ {
901
+ messageId: 'suggestArrGenerate',
902
+ output: dedent`
903
+ import { Arr } from 'ts-data-forge';
904
+ declare const xs: readonly number[];
905
+ const ys = Arr.generate(function* () {
906
+ for (const x of xs) {
907
+ if (x > 0) {
908
+ yield x;
909
+ }
910
+ }
911
+ });
912
+ `,
913
+ },
914
+ ],
915
+ },
916
+ ],
917
+ },
918
+ {
919
+ name: 'only suggests when a comment after `flatMap(` would be lost',
920
+ code: dedent`
921
+ declare const xs: readonly number[];
922
+ const ys = xs.flatMap(
923
+ // keep only the positives
924
+ (x) => (x > 0 ? [x] : []),
925
+ );
926
+ `,
927
+ output: null,
928
+ errors: [
929
+ {
930
+ messageId: 'useArrGenerate',
931
+ suggestions: [
932
+ {
933
+ messageId: 'suggestArrGenerate',
934
+ output: dedent`
935
+ import { Arr } from 'ts-data-forge';
936
+ declare const xs: readonly number[];
937
+ const ys = Arr.generate(function* () {
938
+ for (const x of xs) {
939
+ if (x > 0) {
940
+ yield x;
941
+ }
942
+ }
943
+ });
944
+ `,
945
+ },
946
+ ],
947
+ },
948
+ ],
949
+ },
950
+ {
951
+ name: 'only suggests when a comment after the callback, before `)` would be lost',
952
+ code: dedent`
953
+ declare const xs: readonly number[];
954
+ const ys = xs.flatMap(
955
+ (x) => (x > 0 ? [x] : []),
956
+ // keep only the positives
957
+ );
958
+ `,
959
+ output: null,
960
+ errors: [
961
+ {
962
+ messageId: 'useArrGenerate',
963
+ suggestions: [
964
+ {
965
+ messageId: 'suggestArrGenerate',
966
+ output: dedent`
967
+ import { Arr } from 'ts-data-forge';
968
+ declare const xs: readonly number[];
969
+ const ys = Arr.generate(function* () {
970
+ for (const x of xs) {
971
+ if (x > 0) {
972
+ yield x;
973
+ }
974
+ }
975
+ });
976
+ `,
977
+ },
978
+ ],
979
+ },
980
+ ],
981
+ },
982
+ {
983
+ name: 'keeps the fix automatic when the comment is inside the carried type argument',
984
+ code: dedent`
985
+ declare const xs: readonly number[];
986
+ const ys = xs.flatMap</* positives */ number>((x) => (x > 0 ? [x] : []));
987
+ `,
988
+ output: dedent`
989
+ import { Arr } from 'ts-data-forge';
990
+ declare const xs: readonly number[];
991
+ const ys = Arr.generate</* positives */ number>(function* () {
992
+ for (const x of xs) {
993
+ if (x > 0) {
994
+ yield x;
995
+ }
996
+ }
997
+ });
998
+ `,
999
+ errors: [{ messageId: 'useArrGenerate' }],
1000
+ },
1001
+ {
1002
+ name: 'keeps the fix automatic when the comment is inside the receiver or the parameter',
1003
+ code: dedent`
1004
+ declare const f: (a: readonly { a: number }[]) => readonly { a: number }[];
1005
+ declare const xs: readonly { a: number }[];
1006
+ const ys = f(/* all */ xs).flatMap(({ a /* the value */ }) => (a > 0 ? [a] : []));
1007
+ `,
1008
+ output: dedent`
1009
+ import { Arr } from 'ts-data-forge';
1010
+ declare const f: (a: readonly { a: number }[]) => readonly { a: number }[];
1011
+ declare const xs: readonly { a: number }[];
1012
+ const ys = Arr.generate(function* () {
1013
+ for (const { a /* the value */ } of f(/* all */ xs)) {
1014
+ if (a > 0) {
1015
+ yield a;
1016
+ }
1017
+ }
1018
+ });
1019
+ `,
1020
+ errors: [{ messageId: 'useArrGenerate' }],
1021
+ },
1022
+ {
1023
+ name: 'only suggests when a comment in a negated test would be lost',
1024
+ code: dedent`
1025
+ declare const xs: readonly number[];
1026
+ const ys = xs.flatMap((x) => (x > 0 && /* small */ x < 9 ? [] : [x]));
1027
+ `,
1028
+ output: null,
1029
+ errors: [
1030
+ {
1031
+ messageId: 'useArrGenerate',
1032
+ suggestions: [
1033
+ {
1034
+ messageId: 'suggestArrGenerate',
1035
+ output: dedent`
1036
+ import { Arr } from 'ts-data-forge';
1037
+ declare const xs: readonly number[];
1038
+ const ys = Arr.generate(function* () {
1039
+ for (const x of xs) {
1040
+ if (!(x > 0) || !(x < 9)) {
1041
+ yield x;
1042
+ }
1043
+ }
1044
+ });
1045
+ `,
1046
+ },
1047
+ ],
1048
+ },
1049
+ ],
1050
+ },
1051
+ {
1052
+ name: 'only suggests when a comment before `.flatMap` would be lost, even if a kept comment starts with the same text',
1053
+ code: dedent`
1054
+ declare const xs: readonly number[];
1055
+ const ys = xs // TODO
1056
+ .flatMap((x) => {
1057
+ // TODO: check this
1058
+ const y = x + 1;
1059
+ return y > 0 ? [y] : [];
1060
+ });
1061
+ `,
1062
+ output: null,
1063
+ errors: [
1064
+ {
1065
+ messageId: 'useArrGenerate',
1066
+ suggestions: [
1067
+ {
1068
+ messageId: 'suggestArrGenerate',
1069
+ output: dedent`
1070
+ import { Arr } from 'ts-data-forge';
1071
+ declare const xs: readonly number[];
1072
+ const ys = Arr.generate(function* () {
1073
+ for (const x of xs) {
1074
+ // TODO: check this
1075
+ const y = x + 1;
1076
+ if (y > 0) {
1077
+ yield y;
1078
+ }
1079
+ }
1080
+ });
1081
+ `,
1082
+ },
1083
+ ],
1084
+ },
1085
+ ],
1086
+ },
1087
+ {
1088
+ name: 'keeps the fix automatic for a multi-line comment in the prelude',
1089
+ code: dedent`
1090
+ declare const xs: readonly number[];
1091
+ const ys = xs.flatMap((x) => {
1092
+ /*
1093
+ * Odd ones only.
1094
+ */
1095
+ const y = x;
1096
+ return y % 2 === 1 ? [y] : [];
1097
+ });
1098
+ `,
1099
+ output: dedent`
1100
+ import { Arr } from 'ts-data-forge';
1101
+ declare const xs: readonly number[];
1102
+ const ys = Arr.generate(function* () {
1103
+ for (const x of xs) {
1104
+ /*
1105
+ * Odd ones only.
1106
+ */
1107
+ const y = x;
1108
+ if (y % 2 === 1) {
1109
+ yield y;
1110
+ }
1111
+ }
1112
+ });
1113
+ `,
1114
+ errors: [{ messageId: 'useArrGenerate' }],
1115
+ },
887
1116
  {
888
1117
  name: 'only suggests when a comment would be lost',
889
1118
  code: dedent`
@@ -79,8 +79,8 @@ export const preferArrIsFixedLengthArray: TSESLint.RuleModule<
79
79
  mut_nodesToFix.push({
80
80
  node,
81
81
  arrayExpression,
82
- // The literal or constant itself: a wrapper such as `as number`
83
- // would only widen the length the guard narrows to.
82
+ // The literal or constant itself: a wrapper such as `as number` would
83
+ // only widen the length the guard narrows to.
84
84
  lengthExpression: skipTypeWrappers(valueSide),
85
85
  isNegated,
86
86
  });
@@ -85,8 +85,8 @@ export const preferArrIsMaxLengthArray: TSESLint.RuleModule<
85
85
  mut_nodesToFix.push({
86
86
  node,
87
87
  arrayExpression,
88
- // The literal or constant itself: a wrapper such as `as number`
89
- // would only widen the length the guard narrows to.
88
+ // The literal or constant itself: a wrapper such as `as number` would
89
+ // only widen the length the guard narrows to.
90
90
  lengthExpression: skipTypeWrappers(valueSide),
91
91
  });
92
92
  },
@@ -85,8 +85,8 @@ export const preferArrIsMinLengthArray: TSESLint.RuleModule<
85
85
  mut_nodesToFix.push({
86
86
  node,
87
87
  arrayExpression,
88
- // The literal or constant itself: a wrapper such as `as number`
89
- // would only widen the length the guard narrows to.
88
+ // The literal or constant itself: a wrapper such as `as number` would
89
+ // only widen the length the guard narrows to.
90
90
  lengthExpression: skipTypeWrappers(valueSide),
91
91
  });
92
92
  },
@@ -45,8 +45,8 @@ type AccumulationCandidate = Readonly<{
45
45
  call: TSESTree.CallExpression;
46
46
  /**
47
47
  * What the call is replaced with, after the name `Arr` is bound to
48
- * (`scan(xs, f, init)`), or `undefined` when the rewrite could not be
49
- * derived and the report carries no fix.
48
+ * (`scan(xs, f, init)`), or `undefined` when the rewrite could not be derived
49
+ * and the report carries no fix.
50
50
  */
51
51
  replacement: string | undefined;
52
52
  }>;
@@ -280,12 +280,12 @@ export const preferArrScan: TSESLint.RuleModule<MessageIds, Options> = {
280
280
  // `acc.at(-1)` is `T | undefined` only because `at` can go out of bounds,
281
281
  // which a seeded accumulator never does. So once the value `scan` carries
282
282
  // is known not to be nullish, a `?? fallback` guarding the read is dead
283
- // and the fix takes it with the read — leaving it would hand the author an
284
- // unnecessary condition that no rule can fix for them. Every layer of
285
- // type wrappers is asked (see typeWrapperLayers): the value under an
286
- // `as` / `!` is what `scan` carries at run time (`[init!]` may still
287
- // hold `undefined`), and a wider type a wrapper states is the author
288
- // saying it may be nullish.
283
+ // and the fix takes it with the read — leaving it would hand the author
284
+ // an unnecessary condition that no rule can fix for them. Every layer of
285
+ // type wrappers is asked (see typeWrapperLayers): the value under an `as`
286
+ // / `!` is what `scan` carries at run time (`[init!]` may still hold
287
+ // `undefined`), and a wider type a wrapper states is the author saying it
288
+ // may be nullish.
289
289
  const carriesNullish = [seed, element].some((node) =>
290
290
  typeWrapperLayers(node).some(isNullish),
291
291
  );
@@ -67,9 +67,9 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
67
67
 
68
68
  const reducer = node.arguments[0];
69
69
 
70
- // The initial value and the operands of the body are read through
71
- // type wrappers: `0 as number` is still 0, `(b satisfies number)` is
72
- // still `b`. Types are asked of the values underneath too.
70
+ // The initial value and the operands of the body are read through type
71
+ // wrappers: `0 as number` is still 0, `(b satisfies number)` is still
72
+ // `b`. Types are asked of the values underneath too.
73
73
  const initialValue = skipTypeWrappers(node.arguments[1]);
74
74
 
75
75
  // Check initial value is 0
@@ -55,8 +55,8 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
55
55
 
56
56
  const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap;
57
57
 
58
- // Without type information the rule cannot tell an array of primitives
59
- // from an array of objects, and would suggest code that does not compile.
58
+ // Without type information the rule cannot tell an array of primitives from
59
+ // an array of objects, and would suggest code that does not compile.
60
60
  if (checker === undefined || esTreeNodeToTSNodeMap === undefined) {
61
61
  return {};
62
62
  }
@@ -87,8 +87,8 @@ const asNegatedPositiveIntegerLiteral = (
87
87
 
88
88
  /**
89
89
  * Whether re-reading the expression is free of side effects (a plain variable
90
- * or a non-computed property chain, type wrappers aside). Required for
91
- * patterns that reference the receiver twice, such as
90
+ * or a non-computed property chain, type wrappers aside). Required for patterns
91
+ * that reference the receiver twice, such as
92
92
  * `xs.filter((_, i) => i < xs.length - 1)`.
93
93
  */
94
94
  const isSimpleReference = (wrapped: TSESTree.Node): boolean => {
@@ -213,8 +213,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
213
213
  const checker = parserServices.program.getTypeChecker();
214
214
 
215
215
  /**
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
216
+ * The in-scope way to reference the `Arr` namespace, the name the reference
217
+ * starts with, and the edits that bring it into scope: a value
218
218
  * import of `Arr` (possibly aliased), a namespace import member, or `Arr`
219
219
  * bound by {@link bindValueImports}, which adds it to an existing import or
220
220
  * drops `type` from one. `undefined` when none can: `import type { Arr }`
@@ -235,8 +235,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
235
235
  const arrRef = arrTarget?.ref ?? 'Arr';
236
236
 
237
237
  /**
238
- * Deferred (non-concrete) types are reduced to their base constraint
239
- * before checking; `any` / `unknown` are conservatively not array-like.
238
+ * Deferred (non-concrete) types are reduced to their base constraint before
239
+ * checking; `any` / `unknown` are conservatively not array-like.
240
240
  */
241
241
  const isArrayOrTupleType = (type: ts.Type): boolean => {
242
242
  if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
@@ -398,8 +398,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
398
398
 
399
399
  /**
400
400
  * Matches an index-only filter callback (`(_, i) => <comparison>`) and
401
- * classifies the comparison. Both a leading index (`i >= 1`) and a
402
- * trailing index (`1 <= i`) operand order are accepted.
401
+ * classifies the comparison. Both a leading index (`i >= 1`) and a trailing
402
+ * index (`1 <= i`) operand order are accepted.
403
403
  */
404
404
  const classifyFilterCallback = (
405
405
  callback: TSESTree.CallExpressionArgument,
@@ -471,10 +471,10 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
471
471
  return undefined;
472
472
  }
473
473
 
474
- // The element parameter must not participate in the comparison (only
475
- // the index side and a receiver-independent bound are recognized, so
476
- // reaching here with an element reference is impossible for literals,
477
- // but `xs.length - N` needs a stable receiver).
474
+ // The element parameter must not participate in the comparison (only the
475
+ // index side and a receiver-independent bound are recognized, so reaching
476
+ // here with an element reference is impossible for literals, but
477
+ // `xs.length - N` needs a stable receiver).
478
478
  const n = asNonNegativeIntegerLiteral(other);
479
479
 
480
480
  if (n !== undefined) {
@@ -724,8 +724,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
724
724
  return;
725
725
  }
726
726
 
727
- // The receiver must be an array or tuple (not a string / typed array
728
- // / other object that happens to share the method name).
727
+ // The receiver must be an array or tuple (not a string / typed array /
728
+ // other object that happens to share the method name).
729
729
  if (!holdsThroughWrappers(receiver, isArrayOrTupleType)) {
730
730
  return;
731
731
  }
@@ -798,8 +798,8 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
798
798
  return;
799
799
  }
800
800
 
801
- // `xs.concat(item)` appends only when `item` can never be an
802
- // array (otherwise `concat` would flatten it).
801
+ // `xs.concat(item)` appends only when `item` can never be an array
802
+ // (otherwise `concat` would flatten it).
803
803
  if (holdsThroughWrappers(arg, isDefinitelyNotArrayType)) {
804
804
  report(
805
805
  node,
@@ -839,8 +839,7 @@ export const preferCanonicalArraySlicing: TSESLint.RuleModule<
839
839
  : [undefined, undefined, undefined];
840
840
 
841
841
  // `spread`, `item`, and `fn` are set together (all from the same
842
- // tuple), so checking `spread` also guarantees the others are
843
- // defined.
842
+ // tuple), so checking `spread` also guarantees the others are defined.
844
843
  if (spread === undefined) {
845
844
  return;
846
845
  }
@@ -288,10 +288,10 @@ const matchesBounds = (
288
288
  /**
289
289
  * Removes the length arguments the rewrite drops. Each removal runs from the
290
290
  * first token after the delimiter before the dropped argument (the call's `(`
291
- * or the previous comma) up to the first token after the comma that follows
292
- * it. Parentheses the source put around the dropped argument, the comma,
293
- * whitespace and any comments in between go with it; parentheses around the
294
- * next argument stay. The ranges never overlap.
291
+ * or the previous comma) up to the first token after the comma that follows it.
292
+ * Parentheses the source put around the dropped argument, the comma, whitespace
293
+ * and any comments in between go with it; parentheses around the next argument
294
+ * stay. The ranges never overlap.
295
295
  */
296
296
  const dropBoundsFix = (
297
297
  fixer: TSESLint.RuleFixer,
@@ -303,13 +303,13 @@ const dropBoundsFix = (
303
303
  rewrite: CastRewrite,
304
304
  ): readonly TSESLint.RuleFix[] =>
305
305
  rewrite.bounds.flatMap((_, index) => {
306
- // Read before the `includes` below, not after. The strict standard
307
- // library types `includes` as `searchElement is T` — that is what lets a
308
- // literal union narrow a wider value — but the predicate applies to the
309
- // false branch too, and here `keep` is `readonly number[]` against a
310
- // `number`, so `index` narrows to `never` past the guard and `index + 1`
311
- // stops type-checking. A false `includes` means "not in this array", not
312
- // "not of this type".
306
+ // Read before the `includes` below, not after. The strict standard library
307
+ // types `includes` as `searchElement is T` — that is what lets a literal
308
+ // union narrow a wider value — but the predicate applies to the false
309
+ // branch too, and here `keep` is `readonly number[]` against a `number`, so
310
+ // `index` narrows to `never` past the guard and `index + 1` stops
311
+ // type-checking. A false `includes` means "not in this array", not "not of
312
+ // this type".
313
313
  const dropped = node.arguments[index];
314
314
 
315
315
  const previous = node.arguments[index - 1];
@@ -59,8 +59,8 @@ const MESSAGES = {
59
59
  } as const satisfies ReadonlyRecord<MessageIds, string>;
60
60
 
61
61
  /**
62
- * Length guards whose bound makes them redundant with a named degenerate
63
- * guard, keyed by guard name.
62
+ * Length guards whose bound makes them redundant with a named degenerate guard,
63
+ * keyed by guard name.
64
64
  *
65
65
  * Every entry stays inside its own family rather than crossing the
66
66
  * branded/structural divide: