eslint-plugin-ts-data-forge 0.8.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +20 -19
  2. package/dist/plugin.d.mts +2 -0
  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 +16 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rule-types.mjs.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +21 -2
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +51 -4
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/constants.d.mts +2 -0
  14. package/dist/rules/constants.d.mts.map +1 -1
  15. package/dist/rules/constants.mjs +9 -0
  16. package/dist/rules/constants.mjs.map +1 -1
  17. package/dist/rules/mutable-result.d.mts +10 -0
  18. package/dist/rules/mutable-result.d.mts.map +1 -0
  19. package/dist/rules/mutable-result.mjs +145 -0
  20. package/dist/rules/mutable-result.mjs.map +1 -0
  21. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  22. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  23. package/dist/rules/prefer-arr-generate.mjs +588 -0
  24. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  25. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-uniq.mjs +2 -149
  27. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  28. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  29. package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
  30. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  31. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  32. package/dist/rules/prefer-is-non-null-object.mjs +110 -40
  33. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  34. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  35. package/dist/rules/prefer-is-record-and-has-key.mjs +102 -45
  36. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  37. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  38. package/dist/rules/prefer-range-for-loop.mjs +97 -16
  39. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  40. package/dist/rules/rules.d.mts +1 -0
  41. package/dist/rules/rules.d.mts.map +1 -1
  42. package/dist/rules/rules.mjs +2 -0
  43. package/dist/rules/rules.mjs.map +1 -1
  44. package/package.json +11 -11
  45. package/src/plugin.mts +1 -0
  46. package/src/rule-types.mts +17 -1
  47. package/src/rules/ast-utils.mts +72 -5
  48. package/src/rules/constants.mts +12 -0
  49. package/src/rules/mutable-result.mts +204 -0
  50. package/src/rules/prefer-arr-generate.mts +918 -0
  51. package/src/rules/prefer-arr-generate.test.mts +967 -0
  52. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  53. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  54. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  55. package/src/rules/prefer-arr-uniq.mts +2 -207
  56. package/src/rules/prefer-canonical-array-slicing.mts +57 -40
  57. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  58. package/src/rules/prefer-is-non-null-object.mts +160 -57
  59. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  60. package/src/rules/prefer-is-record-and-has-key.mts +148 -67
  61. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  62. package/src/rules/prefer-range-for-loop.mts +142 -21
  63. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  64. package/src/rules/rules.mts +2 -0
@@ -0,0 +1,204 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import * as ts from 'typescript';
7
+ import { primitiveTypeFlags } from './constants.mjs';
8
+
9
+ /**
10
+ * Whether the code around `node` visibly relies on the result being a mutable
11
+ * array, so that swapping in the readonly array `Arr.uniq` or `Arr.generate`
12
+ * returns would not compile. Looks at `node` itself and, when it initializes a variable without a
13
+ * type annotation, at every read of that variable.
14
+ */
15
+ export const needsMutableResult = (
16
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
17
+ sourceCode: TSESLint.SourceCode,
18
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
19
+ checker: ts.TypeChecker,
20
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
21
+ getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
22
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
23
+ node: TSESTree.Node,
24
+ ): boolean => {
25
+ const outer = outermostTypePreservingWrapper(node);
26
+
27
+ if (isUsedAsMutable(checker, getTsNode, outer)) {
28
+ return true;
29
+ }
30
+
31
+ const parent = outer.parent;
32
+
33
+ if (
34
+ parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
35
+ parent.init !== outer ||
36
+ parent.id.type !== AST_NODE_TYPES.Identifier ||
37
+ parent.id.typeAnnotation !== undefined
38
+ ) {
39
+ return false;
40
+ }
41
+
42
+ const [variable] = sourceCode.getDeclaredVariables(parent);
43
+
44
+ return (
45
+ variable?.references.some(
46
+ (reference) =>
47
+ reference.init !== true &&
48
+ isUsedAsMutable(
49
+ checker,
50
+ getTsNode,
51
+ outermostTypePreservingWrapper(reference.identifier),
52
+ ),
53
+ ) ?? false
54
+ );
55
+ };
56
+
57
+ /**
58
+ * `node`, or the outermost `!` / `satisfies` around it. Neither changes the
59
+ * type the value is used at, so `Array.from(new Set(xs))!.push(4)` and
60
+ * `ys!.push(4)` still need a mutable array. An `as` does change it, and a
61
+ * readonly array may be cast to a mutable one, so the walk stops there.
62
+ */
63
+ const outermostTypePreservingWrapper = (
64
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
65
+ node: TSESTree.Node,
66
+ ): TSESTree.Node => {
67
+ const { parent } = node;
68
+
69
+ return parent !== undefined &&
70
+ (parent.type === AST_NODE_TYPES.TSNonNullExpression ||
71
+ parent.type === AST_NODE_TYPES.TSSatisfiesExpression) &&
72
+ parent.expression === node
73
+ ? outermostTypePreservingWrapper(parent)
74
+ : node;
75
+ };
76
+
77
+ const isUsedAsMutable = (
78
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
79
+ checker: ts.TypeChecker,
80
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
81
+ getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
82
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
83
+ node: TSESTree.Node,
84
+ ): boolean => {
85
+ const parent = node.parent;
86
+
87
+ if (
88
+ parent?.type === AST_NODE_TYPES.MemberExpression &&
89
+ parent.object === node
90
+ ) {
91
+ // xs.push(...), xs.sort(), ...
92
+ if (
93
+ !parent.computed &&
94
+ parent.property.type === AST_NODE_TYPES.Identifier &&
95
+ mutatingMethods.has(parent.property.name)
96
+ ) {
97
+ return true;
98
+ }
99
+
100
+ // xs[0] = ..., xs.length = ..., xs[0]++, delete xs[0]
101
+ const grandParent = parent.parent;
102
+
103
+ return (
104
+ (grandParent.type === AST_NODE_TYPES.AssignmentExpression &&
105
+ grandParent.left === parent) ||
106
+ grandParent.type === AST_NODE_TYPES.UpdateExpression ||
107
+ (grandParent.type === AST_NODE_TYPES.UnaryExpression &&
108
+ grandParent.operator === 'delete')
109
+ );
110
+ }
111
+
112
+ // Passed to a parameter, assigned to a variable, or returned from a function
113
+ // whose declared type is a mutable array.
114
+ const tsNode = getTsNode(node);
115
+
116
+ if (tsNode === undefined) {
117
+ return false;
118
+ }
119
+
120
+ if (!ts.isExpression(tsNode)) {
121
+ return false;
122
+ }
123
+
124
+ const expectedType =
125
+ getDeclaredParameterType(checker, tsNode) ??
126
+ checker.getContextualType(tsNode);
127
+
128
+ return (
129
+ expectedType !== undefined && isMutableArrayOnly(checker, expectedType)
130
+ );
131
+ };
132
+
133
+ /**
134
+ * The declared type of the parameter `argument` is passed to. The contextual
135
+ * type of an argument to a generic function is instantiated from that very
136
+ * argument — `Arr.toSorted(Array.from(set), cmp)` has `Ar` inferred as
137
+ * `string[]` — so it would read as "a mutable array is required" for any
138
+ * generic parameter. The declared type (`Ar extends readonly unknown[]`) is
139
+ * what the rewritten argument actually has to satisfy.
140
+ */
141
+ const getDeclaredParameterType = (
142
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
143
+ checker: ts.TypeChecker,
144
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
145
+ argument: ts.Expression,
146
+ ): ts.Type | undefined => {
147
+ const call = argument.parent;
148
+
149
+ if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) {
150
+ return undefined;
151
+ }
152
+
153
+ const index = call.arguments?.indexOf(argument) ?? -1;
154
+
155
+ if (index === -1) {
156
+ return undefined;
157
+ }
158
+
159
+ const declaration = checker.getResolvedSignature(call)?.getDeclaration();
160
+
161
+ const parameter = declaration?.parameters[index];
162
+
163
+ // A rest parameter's declared type is the array of arguments, not the type of
164
+ // one; the contextual type is the better answer there.
165
+ return parameter === undefined || parameter.dotDotDotToken !== undefined
166
+ ? undefined
167
+ : checker.getTypeAtLocation(parameter);
168
+ };
169
+
170
+ /**
171
+ * Whether a readonly array cannot be assigned to `type`: it is a mutable array,
172
+ * or a union of mutable arrays and primitives. (A tuple accepted no array to
173
+ * begin with, so it does not matter here.)
174
+ */
175
+ const isMutableArrayOnly = (
176
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
177
+ checker: ts.TypeChecker,
178
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
179
+ type: ts.Type,
180
+ ): boolean => {
181
+ const members = type.isUnion() ? type.types : ([type] as const);
182
+
183
+ return (
184
+ members.every(
185
+ (t) =>
186
+ // `null`, `undefined` and other primitives accept no array at all.
187
+ (t.flags & primitiveTypeFlags) !== 0 ||
188
+ (checker.isArrayType(t) && t.getSymbol()?.getName() === 'Array'),
189
+ ) && members.some((t) => (t.flags & primitiveTypeFlags) === 0)
190
+ );
191
+ };
192
+
193
+ /** Methods that `ReadonlyArray` lacks and that mutate the receiver. */
194
+ const mutatingMethods: ReadonlySet<string> = new Set([
195
+ 'copyWithin',
196
+ 'fill',
197
+ 'pop',
198
+ 'push',
199
+ 'reverse',
200
+ 'shift',
201
+ 'sort',
202
+ 'splice',
203
+ 'unshift',
204
+ ]);