eslint-plugin-ts-data-forge 0.8.6 → 0.10.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 +21 -19
- package/dist/plugin.d.mts +4 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +31 -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-pipe-map-nullable.d.mts +24 -0
- package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -0
- package/dist/rules/prefer-pipe-map-nullable.mjs +266 -0
- package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -0
- 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 +2 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +4 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -11
- package/src/plugin.mts +2 -0
- package/src/rule-types.mts +33 -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-pipe-map-nullable.mts +391 -0
- package/src/rules/prefer-pipe-map-nullable.test.mts +470 -0
- 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 +4 -0
package/src/rule-types.mts
CHANGED
|
@@ -72,6 +72,21 @@ namespace PreferCanonicalLengthCast {
|
|
|
72
72
|
export type RuleEntry = Linter.StringSeverity;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @description Replace the filter-and-map idiom `xs.flatMap((x) => (cond ? [f(x)] : []))` with a loop in `Arr.generate` from ts-data-forge.
|
|
77
|
+
*
|
|
78
|
+
* ```md
|
|
79
|
+
* | key | value |
|
|
80
|
+
* | :--------- | :--------- |
|
|
81
|
+
* | type | suggestion |
|
|
82
|
+
* | deprecated | false |
|
|
83
|
+
* | fixable | code |
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
namespace PreferArrGenerate {
|
|
87
|
+
export type RuleEntry = Linter.StringSeverity;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
/**
|
|
76
91
|
* @description Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.
|
|
77
92
|
*
|
|
@@ -148,7 +163,7 @@ namespace PreferAsInt {
|
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
/**
|
|
151
|
-
* @description Replace `typeof u === "object" && u !== null
|
|
166
|
+
* @description Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-data-forge.
|
|
152
167
|
*
|
|
153
168
|
* ```md
|
|
154
169
|
* | key | value |
|
|
@@ -310,10 +325,26 @@ namespace PreferObjOverEntriesRoundTrip {
|
|
|
310
325
|
export type RuleEntry = Linter.StringSeverity;
|
|
311
326
|
}
|
|
312
327
|
|
|
328
|
+
/**
|
|
329
|
+
* @description Replace `pipe(x).map(mapNullable(fn))` with `pipe(x).mapNullable(fn)`.
|
|
330
|
+
*
|
|
331
|
+
* ```md
|
|
332
|
+
* | key | value |
|
|
333
|
+
* | :--------- | :--------- |
|
|
334
|
+
* | type | suggestion |
|
|
335
|
+
* | deprecated | false |
|
|
336
|
+
* | fixable | code |
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
namespace PreferPipeMapNullable {
|
|
340
|
+
export type RuleEntry = Linter.StringSeverity;
|
|
341
|
+
}
|
|
342
|
+
|
|
313
343
|
export type EslintTsDataForgeRules = Readonly<{
|
|
314
344
|
'ts-data-forge/prefer-canonical-array-slicing': PreferCanonicalArraySlicing.RuleEntry;
|
|
315
345
|
'ts-data-forge/prefer-canonical-length-guard': PreferCanonicalLengthGuard.RuleEntry;
|
|
316
346
|
'ts-data-forge/prefer-canonical-length-cast': PreferCanonicalLengthCast.RuleEntry;
|
|
347
|
+
'ts-data-forge/prefer-arr-generate': PreferArrGenerate.RuleEntry;
|
|
317
348
|
'ts-data-forge/prefer-arr-is-array': PreferArrIsArray.RuleEntry;
|
|
318
349
|
'ts-data-forge/prefer-arr-scan': PreferArrScan.RuleEntry;
|
|
319
350
|
'ts-data-forge/prefer-arr-sum': PreferArrSum.RuleEntry;
|
|
@@ -328,6 +359,7 @@ export type EslintTsDataForgeRules = Readonly<{
|
|
|
328
359
|
'ts-data-forge/no-unnecessary-type-guard': NoUnnecessaryTypeGuard.RuleEntry;
|
|
329
360
|
'ts-data-forge/prefer-comparison-over-nullish-guard': PreferComparisonOverNullishGuard.RuleEntry;
|
|
330
361
|
'ts-data-forge/prefer-obj-over-entries-round-trip': PreferObjOverEntriesRoundTrip.RuleEntry;
|
|
362
|
+
'ts-data-forge/prefer-pipe-map-nullable': PreferPipeMapNullable.RuleEntry;
|
|
331
363
|
}>;
|
|
332
364
|
|
|
333
365
|
export type EslintTsDataForgeRulesOption = Readonly<{
|
package/src/rules/ast-utils.mts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
typeWrapperLayers,
|
|
11
11
|
} from 'eslint-rule-forge';
|
|
12
12
|
import type { DeepReadonly } from 'ts-type-forge';
|
|
13
|
+
import type * as ts from 'typescript';
|
|
13
14
|
|
|
14
15
|
const isIntegerLiteral = (node: DeepReadonly<TSESTree.Expression>): boolean =>
|
|
15
16
|
node.type === AST_NODE_TYPES.Literal &&
|
|
@@ -144,7 +145,8 @@ type LengthComparison = Readonly<{
|
|
|
144
145
|
* Parses a `<array>.length <op> <bound>` comparison (in either operand order)
|
|
145
146
|
* into its array expression, integer-literal/`const` `bound`, and whether it is
|
|
146
147
|
* a lower (`min`) or upper (`max`) length bound. Returns `undefined` for
|
|
147
|
-
* anything else. The length and the bound are read through
|
|
148
|
+
* anything else. The comparison, the length and the bound are read through
|
|
149
|
+
* type wrappers; the
|
|
148
150
|
* `bound` returned is the literal or constant underneath, since a wrapper such
|
|
149
151
|
* as `as number` would only widen the length a guard narrows to.
|
|
150
152
|
*
|
|
@@ -153,10 +155,12 @@ type LengthComparison = Readonly<{
|
|
|
153
155
|
*/
|
|
154
156
|
export const parseLengthComparison = (
|
|
155
157
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
156
|
-
|
|
158
|
+
wrapped: TSESTree.Expression,
|
|
157
159
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
158
160
|
sourceCode: TSESLint.SourceCode,
|
|
159
161
|
): LengthComparison | undefined => {
|
|
162
|
+
const node = skipTypeWrappers(wrapped);
|
|
163
|
+
|
|
160
164
|
if (
|
|
161
165
|
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
162
166
|
(node.operator !== '>=' && node.operator !== '<=')
|
|
@@ -201,6 +205,8 @@ export const parseLengthComparison = (
|
|
|
201
205
|
* together form the `Arr.isBoundedLengthArray` pattern handled by
|
|
202
206
|
* `prefer-arr-is-bounded-length-array`. The single-bound rules skip such
|
|
203
207
|
* operands so the bounded rule rewrites the whole `&&` instead of the parts.
|
|
208
|
+
* A comparison wrapped as a whole (`(xs.length >= 1 satisfies boolean) && …`)
|
|
209
|
+
* is still the operand.
|
|
204
210
|
*/
|
|
205
211
|
export const isPartOfBoundedLengthCheck = (
|
|
206
212
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -208,10 +214,12 @@ export const isPartOfBoundedLengthCheck = (
|
|
|
208
214
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
209
215
|
sourceCode: TSESLint.SourceCode,
|
|
210
216
|
): boolean => {
|
|
211
|
-
const
|
|
217
|
+
const operand = outermostTypeWrapper(node);
|
|
218
|
+
|
|
219
|
+
const { parent } = operand;
|
|
212
220
|
|
|
213
221
|
if (
|
|
214
|
-
parent
|
|
222
|
+
parent?.type !== AST_NODE_TYPES.LogicalExpression ||
|
|
215
223
|
parent.operator !== '&&'
|
|
216
224
|
) {
|
|
217
225
|
return false;
|
|
@@ -223,7 +231,7 @@ export const isPartOfBoundedLengthCheck = (
|
|
|
223
231
|
return false;
|
|
224
232
|
}
|
|
225
233
|
|
|
226
|
-
const sibling = parent.left ===
|
|
234
|
+
const sibling = parent.left === operand ? parent.right : parent.left;
|
|
227
235
|
|
|
228
236
|
const other = parseLengthComparison(sibling, sourceCode);
|
|
229
237
|
|
|
@@ -237,6 +245,15 @@ export const isPartOfBoundedLengthCheck = (
|
|
|
237
245
|
);
|
|
238
246
|
};
|
|
239
247
|
|
|
248
|
+
/** The outermost of the type wrappers around `node`, or `node` itself. */
|
|
249
|
+
const outermostTypeWrapper = (
|
|
250
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
251
|
+
node: TSESTree.Node,
|
|
252
|
+
): TSESTree.Node =>
|
|
253
|
+
node.parent !== undefined && isTypeWrapper(node.parent)
|
|
254
|
+
? outermostTypeWrapper(node.parent)
|
|
255
|
+
: node;
|
|
256
|
+
|
|
240
257
|
type VariableDefinition = Readonly<{
|
|
241
258
|
type: string;
|
|
242
259
|
parent: TSESTree.VariableDeclaration;
|
|
@@ -248,3 +265,53 @@ const isVariableDefinition = (
|
|
|
248
265
|
def: TSESLint.Scope.Definition,
|
|
249
266
|
): def is TSESLint.Scope.Definition & VariableDefinition =>
|
|
250
267
|
(def.type as string) === 'Variable';
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Whether `node`, read through its type wrappers, is the identifier `name`:
|
|
271
|
+
* `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
|
|
272
|
+
*/
|
|
273
|
+
export const isIdentifierNamed = (
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
275
|
+
node: TSESTree.Node,
|
|
276
|
+
name: string,
|
|
277
|
+
): boolean => {
|
|
278
|
+
const unwrapped = skipTypeWrappers(node);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
unwrapped.type === AST_NODE_TYPES.Identifier && unwrapped.name === name
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Whether `holds` is true of the type of every layer of type wrappers around
|
|
287
|
+
* `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
|
|
288
|
+
* without type information. A widening cast (`s as string | number`) is the
|
|
289
|
+
* author saying the value may be more than it is, and is respected; a
|
|
290
|
+
* narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
|
|
291
|
+
* value's own type does not, and the fix, which keeps the wrapper, has to
|
|
292
|
+
* type-check at every layer too.
|
|
293
|
+
*/
|
|
294
|
+
export const typeHoldsAtEveryLayer = (
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
296
|
+
sourceCode: TSESLint.SourceCode,
|
|
297
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
298
|
+
node: TSESTree.Node,
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
300
|
+
holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean,
|
|
301
|
+
): boolean => {
|
|
302
|
+
const services = sourceCode.parserServices;
|
|
303
|
+
|
|
304
|
+
const checker = services?.program?.getTypeChecker();
|
|
305
|
+
|
|
306
|
+
if (checker === undefined) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return typeWrapperLayers(node).every((layer) => {
|
|
311
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker)
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
};
|
package/src/rules/constants.mts
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
|
|
1
3
|
export const TS_DATA_FORGE_MODULE = 'ts-data-forge';
|
|
4
|
+
|
|
5
|
+
/** The type flags of every primitive type but `void`. */
|
|
6
|
+
export const primitiveTypeFlags =
|
|
7
|
+
ts.TypeFlags.StringLike |
|
|
8
|
+
ts.TypeFlags.NumberLike |
|
|
9
|
+
ts.TypeFlags.BigIntLike |
|
|
10
|
+
ts.TypeFlags.BooleanLike |
|
|
11
|
+
ts.TypeFlags.ESSymbolLike |
|
|
12
|
+
ts.TypeFlags.Null |
|
|
13
|
+
ts.TypeFlags.Undefined;
|
|
@@ -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
|
+
]);
|