eslint-plugin-ts-data-forge 0.7.4 → 0.8.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 +1 -0
- 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 +15 -0
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts +15 -0
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -0
- package/dist/rules/prefer-arr-uniq.mjs +350 -0
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -0
- 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 +4 -2
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +16 -0
- package/src/rules/prefer-arr-uniq.mts +475 -0
- package/src/rules/prefer-arr-uniq.test.mts +455 -0
- package/src/rules/rules.mts +2 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { Arr } from 'ts-data-forge';
|
|
7
|
+
import * as ts from 'typescript';
|
|
8
|
+
import {
|
|
9
|
+
buildImportFixes,
|
|
10
|
+
getImportedLocalName,
|
|
11
|
+
getTsDataForgeImport,
|
|
12
|
+
} from './import-utils.mjs';
|
|
13
|
+
|
|
14
|
+
type Options = readonly [];
|
|
15
|
+
|
|
16
|
+
type MessageIds = 'useArrUniq' | 'suggestArrUniq';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `Arr.uniq(xs)` is implemented as `Array.from(new Set(xs))`, so the rewrite
|
|
20
|
+
* does not change behavior. What it does change is the static type: `Arr.uniq`
|
|
21
|
+
* only accepts `readonly Primitive[]` and returns a readonly array. The rule
|
|
22
|
+
* therefore reports only when the argument is an array or tuple of primitives,
|
|
23
|
+
* and it applies the fix automatically only when nothing in sight needs the
|
|
24
|
+
* result to be a mutable array; otherwise the rewrite is offered as a
|
|
25
|
+
* suggestion.
|
|
26
|
+
*/
|
|
27
|
+
export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'suggestion',
|
|
30
|
+
docs: {
|
|
31
|
+
description:
|
|
32
|
+
'Replace `Array.from(new Set(xs))` / `[...new Set(xs)]` with `Arr.uniq(xs)` from ts-data-forge.',
|
|
33
|
+
},
|
|
34
|
+
fixable: 'code',
|
|
35
|
+
hasSuggestions: true,
|
|
36
|
+
schema: [],
|
|
37
|
+
messages: {
|
|
38
|
+
useArrUniq: 'Replace with `Arr.uniq({{arrayName}})` from ts-data-forge.',
|
|
39
|
+
suggestArrUniq:
|
|
40
|
+
'Replace with `Arr.uniq({{arrayName}})` from ts-data-forge (the result becomes a readonly array).',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
create: (context) => {
|
|
45
|
+
const sourceCode = context.sourceCode;
|
|
46
|
+
|
|
47
|
+
const program = sourceCode.ast;
|
|
48
|
+
|
|
49
|
+
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
50
|
+
|
|
51
|
+
const services = sourceCode.parserServices;
|
|
52
|
+
|
|
53
|
+
const checker = services?.program?.getTypeChecker();
|
|
54
|
+
|
|
55
|
+
const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap;
|
|
56
|
+
|
|
57
|
+
// Without type information the rule cannot tell an array of primitives
|
|
58
|
+
// from an array of objects, and would suggest code that does not compile.
|
|
59
|
+
if (checker === undefined || esTreeNodeToTSNodeMap === undefined) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const getTsNode = (
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
65
|
+
node: TSESTree.Node,
|
|
66
|
+
): ts.Node | undefined => esTreeNodeToTSNodeMap.get(node);
|
|
67
|
+
|
|
68
|
+
const mut_matches: {
|
|
69
|
+
node: TSESTree.CallExpression | TSESTree.ArrayExpression;
|
|
70
|
+
arrayExpression: TSESTree.Expression;
|
|
71
|
+
autofixable: boolean;
|
|
72
|
+
}[] = [];
|
|
73
|
+
|
|
74
|
+
const check = (
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
76
|
+
node: TSESTree.CallExpression | TSESTree.ArrayExpression,
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
78
|
+
setNode: TSESTree.Node,
|
|
79
|
+
): void => {
|
|
80
|
+
const arrayExpression = getSetSourceArray(setNode);
|
|
81
|
+
|
|
82
|
+
if (arrayExpression === undefined) return;
|
|
83
|
+
|
|
84
|
+
if (!isGlobal(sourceCode, node, 'Set')) return;
|
|
85
|
+
|
|
86
|
+
const arrayTsNode = getTsNode(arrayExpression);
|
|
87
|
+
|
|
88
|
+
if (arrayTsNode === undefined) return;
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
!isPrimitiveArrayType(checker, checker.getTypeAtLocation(arrayTsNode))
|
|
92
|
+
)
|
|
93
|
+
return;
|
|
94
|
+
|
|
95
|
+
mut_matches.push({
|
|
96
|
+
node,
|
|
97
|
+
arrayExpression,
|
|
98
|
+
autofixable: !needsMutableResult(sourceCode, checker, getTsNode, node),
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
CallExpression: (node) => {
|
|
104
|
+
// Array.from(new Set(xs))
|
|
105
|
+
if (
|
|
106
|
+
node.optional ||
|
|
107
|
+
node.callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
108
|
+
node.callee.computed ||
|
|
109
|
+
node.callee.optional ||
|
|
110
|
+
node.callee.object.type !== AST_NODE_TYPES.Identifier ||
|
|
111
|
+
node.callee.object.name !== 'Array' ||
|
|
112
|
+
node.callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
113
|
+
node.callee.property.name !== 'from' ||
|
|
114
|
+
// `Array.from(set, fn)` maps as well, which `Arr.uniq` does not.
|
|
115
|
+
!Arr.isFixedLengthTuple(1, node.arguments)
|
|
116
|
+
) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!isGlobal(sourceCode, node, 'Array')) return;
|
|
121
|
+
|
|
122
|
+
check(node, node.arguments[0]);
|
|
123
|
+
},
|
|
124
|
+
ArrayExpression: (node) => {
|
|
125
|
+
// [...new Set(xs)]
|
|
126
|
+
if (!Arr.isFixedLengthTuple(1, node.elements)) return;
|
|
127
|
+
|
|
128
|
+
const element = node.elements[0];
|
|
129
|
+
|
|
130
|
+
if (element?.type !== AST_NODE_TYPES.SpreadElement) return;
|
|
131
|
+
|
|
132
|
+
check(node, element.argument);
|
|
133
|
+
},
|
|
134
|
+
'Program:exit': () => {
|
|
135
|
+
const arrLocalName = getImportedLocalName(tsDataForgeImport, 'Arr');
|
|
136
|
+
|
|
137
|
+
const arrName = arrLocalName ?? 'Arr';
|
|
138
|
+
|
|
139
|
+
// The import is added by the first fix only, so that several fixes do
|
|
140
|
+
// not insert it at the same position. When the first match is not
|
|
141
|
+
// autofixable, the first autofixable one carries it instead.
|
|
142
|
+
const firstAutofixable = mut_matches.find((m) => m.autofixable);
|
|
143
|
+
|
|
144
|
+
for (const match of mut_matches) {
|
|
145
|
+
const arrayText = sourceCode.getText(match.arrayExpression);
|
|
146
|
+
|
|
147
|
+
const argumentText =
|
|
148
|
+
match.arrayExpression.type === AST_NODE_TYPES.SequenceExpression
|
|
149
|
+
? `(${arrayText})`
|
|
150
|
+
: arrayText;
|
|
151
|
+
|
|
152
|
+
const buildFix =
|
|
153
|
+
(withImport: boolean): TSESLint.ReportFixFunction =>
|
|
154
|
+
(fixer) => [
|
|
155
|
+
...(withImport && arrLocalName === undefined
|
|
156
|
+
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
157
|
+
: []),
|
|
158
|
+
fixer.replaceText(match.node, `${arrName}.uniq(${argumentText})`),
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const data = { arrayName: arrayText } as const;
|
|
162
|
+
|
|
163
|
+
if (match.autofixable) {
|
|
164
|
+
context.report({
|
|
165
|
+
node: match.node,
|
|
166
|
+
messageId: 'useArrUniq',
|
|
167
|
+
data,
|
|
168
|
+
fix: buildFix(match === firstAutofixable),
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
context.report({
|
|
172
|
+
node: match.node,
|
|
173
|
+
messageId: 'useArrUniq',
|
|
174
|
+
data,
|
|
175
|
+
suggest: [
|
|
176
|
+
{
|
|
177
|
+
messageId: 'suggestArrUniq',
|
|
178
|
+
data,
|
|
179
|
+
fix: buildFix(true),
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
defaultOptions: [],
|
|
189
|
+
} as const;
|
|
190
|
+
|
|
191
|
+
/** Methods that `ReadonlyArray` lacks and that mutate the receiver. */
|
|
192
|
+
const mutatingMethods: ReadonlySet<string> = new Set([
|
|
193
|
+
'copyWithin',
|
|
194
|
+
'fill',
|
|
195
|
+
'pop',
|
|
196
|
+
'push',
|
|
197
|
+
'reverse',
|
|
198
|
+
'shift',
|
|
199
|
+
'sort',
|
|
200
|
+
'splice',
|
|
201
|
+
'unshift',
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Returns `xs` for `new Set(xs)`, and `undefined` for anything else — including
|
|
206
|
+
* `new Set()`, `new Set<T>(xs)` (whose element type may be wider than `xs`'s)
|
|
207
|
+
* and `new Set(...args)`.
|
|
208
|
+
*/
|
|
209
|
+
const getSetSourceArray = (
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
211
|
+
node: TSESTree.Node,
|
|
212
|
+
): TSESTree.Expression | undefined => {
|
|
213
|
+
if (
|
|
214
|
+
node.type !== AST_NODE_TYPES.NewExpression ||
|
|
215
|
+
node.callee.type !== AST_NODE_TYPES.Identifier ||
|
|
216
|
+
node.callee.name !== 'Set' ||
|
|
217
|
+
node.typeArguments !== undefined ||
|
|
218
|
+
!Arr.isFixedLengthTuple(1, node.arguments)
|
|
219
|
+
) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const argument = node.arguments[0];
|
|
224
|
+
|
|
225
|
+
return argument.type === AST_NODE_TYPES.SpreadElement ? undefined : argument;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** Whether `name` at `node` refers to the built-in global, not a local binding. */
|
|
229
|
+
const isGlobal = (
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
231
|
+
sourceCode: TSESLint.SourceCode,
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
233
|
+
node: TSESTree.Node,
|
|
234
|
+
name: string,
|
|
235
|
+
): boolean => {
|
|
236
|
+
const variable = findVariable(sourceCode.getScope(node), name);
|
|
237
|
+
|
|
238
|
+
// Globals from the TypeScript lib are implicit variables with no definition.
|
|
239
|
+
return variable === undefined || variable.defs.length === 0;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const findVariable = (
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
244
|
+
scope: TSESLint.Scope.Scope,
|
|
245
|
+
name: string,
|
|
246
|
+
): TSESLint.Scope.Variable | undefined => {
|
|
247
|
+
const variable = scope.set.get(name);
|
|
248
|
+
|
|
249
|
+
if (variable !== undefined) return variable;
|
|
250
|
+
|
|
251
|
+
if (scope.upper === null) return undefined;
|
|
252
|
+
|
|
253
|
+
return findVariable(scope.upper, name);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Whether `type` is an array or tuple (or a union or intersection of them) whose elements are
|
|
258
|
+
* all primitives, i.e. something `Arr.uniq` accepts.
|
|
259
|
+
*/
|
|
260
|
+
const isPrimitiveArrayType = (
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
262
|
+
checker: ts.TypeChecker,
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
264
|
+
type: ts.Type,
|
|
265
|
+
): boolean => {
|
|
266
|
+
const resolved = resolveConstraint(checker, type);
|
|
267
|
+
|
|
268
|
+
if (resolved.isUnion()) {
|
|
269
|
+
return resolved.types.every((t) => isPrimitiveArrayType(checker, t));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// `readonly string[] & Brand`, `readonly [...Ar, V] & NonEmptyArray<E>`: one
|
|
273
|
+
// primitive-array member already makes the whole assignable to one.
|
|
274
|
+
if (resolved.isIntersection()) {
|
|
275
|
+
return resolved.types.some((t) => isPrimitiveArrayType(checker, t));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!checker.isArrayType(resolved) && !checker.isTupleType(resolved)) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const typeArguments = checker.getTypeArguments(
|
|
283
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
284
|
+
resolved as ts.TypeReference,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// An empty tuple has nothing to deduplicate, but is still accepted.
|
|
288
|
+
return typeArguments.every((t) => isPrimitiveType(checker, t));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const isPrimitiveType = (
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
293
|
+
checker: ts.TypeChecker,
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
295
|
+
type: ts.Type,
|
|
296
|
+
): boolean => {
|
|
297
|
+
const resolved = resolveConstraint(checker, type);
|
|
298
|
+
|
|
299
|
+
if (resolved.isUnion()) {
|
|
300
|
+
return resolved.types.every((t) => isPrimitiveType(checker, t));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (resolved.flags & primitiveTypeFlags) !== 0;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const primitiveTypeFlags =
|
|
307
|
+
ts.TypeFlags.StringLike |
|
|
308
|
+
ts.TypeFlags.NumberLike |
|
|
309
|
+
ts.TypeFlags.BigIntLike |
|
|
310
|
+
ts.TypeFlags.BooleanLike |
|
|
311
|
+
ts.TypeFlags.ESSymbolLike |
|
|
312
|
+
ts.TypeFlags.Null |
|
|
313
|
+
ts.TypeFlags.Undefined;
|
|
314
|
+
|
|
315
|
+
const resolveConstraint = (
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
317
|
+
checker: ts.TypeChecker,
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
319
|
+
type: ts.Type,
|
|
320
|
+
): ts.Type =>
|
|
321
|
+
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
322
|
+
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
323
|
+
: type;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Whether the code around `node` visibly relies on the result being a mutable
|
|
327
|
+
* array, so that swapping in the readonly array `Arr.uniq` returns would not
|
|
328
|
+
* compile. Looks at `node` itself and, when it initializes a variable without a
|
|
329
|
+
* type annotation, at every read of that variable.
|
|
330
|
+
*/
|
|
331
|
+
const needsMutableResult = (
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
333
|
+
sourceCode: TSESLint.SourceCode,
|
|
334
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
335
|
+
checker: ts.TypeChecker,
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
337
|
+
getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
|
|
338
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
339
|
+
node: TSESTree.Node,
|
|
340
|
+
): boolean => {
|
|
341
|
+
if (isUsedAsMutable(checker, getTsNode, node)) return true;
|
|
342
|
+
|
|
343
|
+
const parent = node.parent;
|
|
344
|
+
|
|
345
|
+
if (
|
|
346
|
+
parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
|
|
347
|
+
parent.init !== node ||
|
|
348
|
+
parent.id.type !== AST_NODE_TYPES.Identifier ||
|
|
349
|
+
parent.id.typeAnnotation !== undefined
|
|
350
|
+
) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const [variable] = sourceCode.getDeclaredVariables(parent);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
variable?.references.some(
|
|
358
|
+
(reference) =>
|
|
359
|
+
reference.init !== true &&
|
|
360
|
+
isUsedAsMutable(checker, getTsNode, reference.identifier),
|
|
361
|
+
) ?? false
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const isUsedAsMutable = (
|
|
366
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
367
|
+
checker: ts.TypeChecker,
|
|
368
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
369
|
+
getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
|
|
370
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
371
|
+
node: TSESTree.Node,
|
|
372
|
+
): boolean => {
|
|
373
|
+
const parent = node.parent;
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
parent?.type === AST_NODE_TYPES.MemberExpression &&
|
|
377
|
+
parent.object === node
|
|
378
|
+
) {
|
|
379
|
+
// xs.push(...), xs.sort(), ...
|
|
380
|
+
if (
|
|
381
|
+
!parent.computed &&
|
|
382
|
+
parent.property.type === AST_NODE_TYPES.Identifier &&
|
|
383
|
+
mutatingMethods.has(parent.property.name)
|
|
384
|
+
) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// xs[0] = ..., xs.length = ..., xs[0]++, delete xs[0]
|
|
389
|
+
const grandParent = parent.parent;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
(grandParent.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
393
|
+
grandParent.left === parent) ||
|
|
394
|
+
grandParent.type === AST_NODE_TYPES.UpdateExpression ||
|
|
395
|
+
(grandParent.type === AST_NODE_TYPES.UnaryExpression &&
|
|
396
|
+
grandParent.operator === 'delete')
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Passed to a parameter, assigned to a variable, or returned from a function
|
|
401
|
+
// whose declared type is a mutable array.
|
|
402
|
+
const tsNode = getTsNode(node);
|
|
403
|
+
|
|
404
|
+
if (tsNode === undefined) return false;
|
|
405
|
+
|
|
406
|
+
if (!ts.isExpression(tsNode)) return false;
|
|
407
|
+
|
|
408
|
+
const expectedType =
|
|
409
|
+
getDeclaredParameterType(checker, tsNode) ??
|
|
410
|
+
checker.getContextualType(tsNode);
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
expectedType !== undefined && isMutableArrayOnly(checker, expectedType)
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The declared type of the parameter `argument` is passed to. The contextual
|
|
419
|
+
* type of an argument to a generic function is instantiated from that very
|
|
420
|
+
* argument — `Arr.toSorted(Array.from(set), cmp)` has `Ar` inferred as
|
|
421
|
+
* `string[]` — so it would read as "a mutable array is required" for any
|
|
422
|
+
* generic parameter. The declared type (`Ar extends readonly unknown[]`) is
|
|
423
|
+
* what the rewritten argument actually has to satisfy.
|
|
424
|
+
*/
|
|
425
|
+
const getDeclaredParameterType = (
|
|
426
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
427
|
+
checker: ts.TypeChecker,
|
|
428
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
429
|
+
argument: ts.Expression,
|
|
430
|
+
): ts.Type | undefined => {
|
|
431
|
+
const call = argument.parent;
|
|
432
|
+
|
|
433
|
+
if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const index = call.arguments?.indexOf(argument) ?? -1;
|
|
438
|
+
|
|
439
|
+
if (index === -1) return undefined;
|
|
440
|
+
|
|
441
|
+
const declaration = checker.getResolvedSignature(call)?.getDeclaration();
|
|
442
|
+
|
|
443
|
+
const parameter = declaration?.parameters[index];
|
|
444
|
+
|
|
445
|
+
// A rest parameter's declared type is the array of arguments, not the type of
|
|
446
|
+
// one; the contextual type is the better answer there.
|
|
447
|
+
if (parameter === undefined || parameter.dotDotDotToken !== undefined) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return checker.getTypeAtLocation(parameter);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Whether a readonly array cannot be assigned to `type`: it is a mutable array,
|
|
456
|
+
* or a union of mutable arrays and primitives. (A tuple accepted no array to
|
|
457
|
+
* begin with, so it does not matter here.)
|
|
458
|
+
*/
|
|
459
|
+
const isMutableArrayOnly = (
|
|
460
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
461
|
+
checker: ts.TypeChecker,
|
|
462
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
463
|
+
type: ts.Type,
|
|
464
|
+
): boolean => {
|
|
465
|
+
const members = type.isUnion() ? type.types : ([type] as const);
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
members.every(
|
|
469
|
+
(t) =>
|
|
470
|
+
// `null`, `undefined` and other primitives accept no array at all.
|
|
471
|
+
(t.flags & primitiveTypeFlags) !== 0 ||
|
|
472
|
+
(checker.isArrayType(t) && t.getSymbol()?.getName() === 'Array'),
|
|
473
|
+
) && members.some((t) => (t.flags & primitiveTypeFlags) === 0)
|
|
474
|
+
);
|
|
475
|
+
};
|