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.
- package/README.md +20 -19
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/ast-utils.d.mts +21 -2
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +51 -4
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +9 -0
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/mutable-result.d.mts +10 -0
- package/dist/rules/mutable-result.d.mts.map +1 -0
- package/dist/rules/mutable-result.mjs +145 -0
- package/dist/rules/mutable-result.mjs.map +1 -0
- package/dist/rules/prefer-arr-generate.d.mts +57 -0
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
- package/dist/rules/prefer-arr-generate.mjs +588 -0
- package/dist/rules/prefer-arr-generate.mjs.map +1 -0
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +2 -149
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +110 -40
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +102 -45
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +97 -16
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -11
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +72 -5
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- package/src/rules/prefer-arr-generate.mts +918 -0
- package/src/rules/prefer-arr-generate.test.mts +967 -0
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-uniq.mts +2 -207
- package/src/rules/prefer-canonical-array-slicing.mts +57 -40
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-is-non-null-object.mts +160 -57
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +148 -67
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-range-for-loop.mts +142 -21
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- 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
|
+
]);
|