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
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
AST_TOKEN_TYPES,
|
|
4
|
+
ASTUtils,
|
|
5
|
+
type TSESLint,
|
|
6
|
+
type TSESTree,
|
|
7
|
+
} from '@typescript-eslint/utils';
|
|
8
|
+
import {
|
|
9
|
+
buildNamedImportFix,
|
|
10
|
+
createGetLayerTypes,
|
|
11
|
+
getArgumentText,
|
|
12
|
+
getImportDeclarations,
|
|
13
|
+
getImportedLocalName,
|
|
14
|
+
negationOf,
|
|
15
|
+
prefixedNegationOf,
|
|
16
|
+
skipTypeWrappers,
|
|
17
|
+
type NegationContext,
|
|
18
|
+
} from 'eslint-rule-forge';
|
|
19
|
+
import { Arr, hasKey, isRecord } from 'ts-data-forge';
|
|
20
|
+
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
21
|
+
import * as ts from 'typescript';
|
|
22
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
23
|
+
import { needsMutableResult } from './mutable-result.mjs';
|
|
24
|
+
|
|
25
|
+
type Options = readonly [];
|
|
26
|
+
|
|
27
|
+
type MessageIds = 'useArrGenerate' | 'suggestArrGenerate';
|
|
28
|
+
|
|
29
|
+
/** A filter-and-map `flatMap` call, taken apart. */
|
|
30
|
+
type Match = Readonly<{
|
|
31
|
+
node: TSESTree.CallExpression;
|
|
32
|
+
replacement: string;
|
|
33
|
+
autofixable: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The callback of `xs.flatMap((x) => (cond ? [a, b] : []))` decides per
|
|
38
|
+
* element whether to keep it and what to keep. Written as a loop, that is
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* Arr.generate(function* () {
|
|
42
|
+
* for (const x of xs) {
|
|
43
|
+
* if (cond) {
|
|
44
|
+
* yield a;
|
|
45
|
+
* yield b;
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* A block-bodied callback whose only `return` is its last statement works the
|
|
52
|
+
* same way: the statements before it move into the loop. When the empty array
|
|
53
|
+
* is the consequent (`cond ? [] : [a]`), the condition is negated the short way
|
|
54
|
+
* `negationOf` from eslint-rule-forge writes it (`a !== b`, `!a && !b`).
|
|
55
|
+
*
|
|
56
|
+
* The callback is reported only when the rewrite keeps its meaning:
|
|
57
|
+
*
|
|
58
|
+
* - The receiver is an array or a tuple at every layer of its type wrappers
|
|
59
|
+
* (an object with a `flatMap` method of its own is not touched).
|
|
60
|
+
* - The callback is a non-async arrow function with one parameter, which may
|
|
61
|
+
* be destructured but has no default: an index or array parameter has no
|
|
62
|
+
* counterpart in the loop.
|
|
63
|
+
* - Neither the callback nor the receiver reads `this`, `arguments`, `super`
|
|
64
|
+
* or `new.target`, which the generator function would rebind; the receiver
|
|
65
|
+
* names none of the parameter's bindings, which the loop variable would
|
|
66
|
+
* shadow (`node.children.flatMap((node) => …)`).
|
|
67
|
+
*
|
|
68
|
+
* The fix is applied automatically only when nothing it drops could have
|
|
69
|
+
* mattered; otherwise it is offered as a suggestion:
|
|
70
|
+
*
|
|
71
|
+
* - The result becomes a readonly array, so code that visibly needs a mutable
|
|
72
|
+
* one (see `needsMutableResult`) keeps a suggestion.
|
|
73
|
+
* - A type annotation on the parameter, a return type on the callback, or a
|
|
74
|
+
* type wrapper around the conditional or either branch
|
|
75
|
+
* (`[] as readonly T[]`), may be what types the elements, and the loop has
|
|
76
|
+
* no place for it.
|
|
77
|
+
* - The receiver moves into the generator function, where a narrowing of a
|
|
78
|
+
* property (`s.decl.items` after `s.decl !== null`) or of a variable that is
|
|
79
|
+
* reassigned somewhere no longer applies.
|
|
80
|
+
* - A `filter()` receiver becomes an intermediate array the loop iterates,
|
|
81
|
+
* which reads better as a condition inside the loop.
|
|
82
|
+
* - A comment the rewrite has no place for would be lost.
|
|
83
|
+
*
|
|
84
|
+
* One difference is not checked: `flatMap` skips the holes of a sparse array,
|
|
85
|
+
* while `for…of` visits them as `undefined`.
|
|
86
|
+
*/
|
|
87
|
+
export const preferArrGenerate: TSESLint.RuleModule<MessageIds, Options> = {
|
|
88
|
+
meta: {
|
|
89
|
+
type: 'suggestion',
|
|
90
|
+
docs: {
|
|
91
|
+
description:
|
|
92
|
+
'Replace the filter-and-map idiom `xs.flatMap((x) => (cond ? [f(x)] : []))` with a loop in `Arr.generate` from ts-data-forge.',
|
|
93
|
+
},
|
|
94
|
+
fixable: 'code',
|
|
95
|
+
hasSuggestions: true,
|
|
96
|
+
schema: [],
|
|
97
|
+
messages: {
|
|
98
|
+
useArrGenerate:
|
|
99
|
+
'Replace this filter-and-map `flatMap` with a loop in `Arr.generate` from ts-data-forge.',
|
|
100
|
+
suggestArrGenerate:
|
|
101
|
+
'Replace with a loop in `Arr.generate` from ts-data-forge (check the element type, and that the result may be readonly).',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
create: (context) => {
|
|
106
|
+
const sourceCode = context.sourceCode;
|
|
107
|
+
|
|
108
|
+
const program = sourceCode.ast;
|
|
109
|
+
|
|
110
|
+
const tsDataForgeImports = getImportDeclarations(
|
|
111
|
+
program,
|
|
112
|
+
TS_DATA_FORGE_MODULE,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const services = sourceCode.parserServices;
|
|
116
|
+
|
|
117
|
+
const tsProgram = services?.program ?? undefined;
|
|
118
|
+
|
|
119
|
+
const esTreeNodeToTSNodeMap = services?.esTreeNodeToTSNodeMap;
|
|
120
|
+
|
|
121
|
+
// Without type information the rule cannot tell an array from another
|
|
122
|
+
// object with a `flatMap` method.
|
|
123
|
+
if (tsProgram === undefined || esTreeNodeToTSNodeMap === undefined) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const checker = tsProgram.getTypeChecker();
|
|
128
|
+
|
|
129
|
+
const getLayerTypes = createGetLayerTypes({
|
|
130
|
+
program: tsProgram,
|
|
131
|
+
esTreeNodeToTSNodeMap,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// The negation is the test of the loop's `if`, where only its truthiness
|
|
135
|
+
// is read, so `!!a` may always be written `a`.
|
|
136
|
+
const negationContext: NegationContext = {
|
|
137
|
+
sourceCode,
|
|
138
|
+
checker,
|
|
139
|
+
getLayerTypes,
|
|
140
|
+
isBoolean: () => true,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const getTsNode = (
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
145
|
+
node: TSESTree.Node,
|
|
146
|
+
): ts.Node | undefined => esTreeNodeToTSNodeMap.get(node);
|
|
147
|
+
|
|
148
|
+
const arrLocalName = getImportedLocalName(tsDataForgeImports, 'Arr');
|
|
149
|
+
|
|
150
|
+
const arrName = arrLocalName ?? 'Arr';
|
|
151
|
+
|
|
152
|
+
const mut_matches: Match[] = [];
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
CallExpression: (node) => {
|
|
156
|
+
const callee = skipTypeWrappers(node.callee);
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
node.optional ||
|
|
160
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
161
|
+
callee.optional ||
|
|
162
|
+
callee.computed ||
|
|
163
|
+
callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
164
|
+
callee.property.name !== 'flatMap' ||
|
|
165
|
+
callee.object.type === AST_NODE_TYPES.Super ||
|
|
166
|
+
// A second argument is `thisArg`.
|
|
167
|
+
!Arr.isFixedLengthTuple(1, node.arguments)
|
|
168
|
+
) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const callback = node.arguments[0];
|
|
173
|
+
|
|
174
|
+
// A wrapper around the callback may be what types its parameter, so
|
|
175
|
+
// a wrapped callback is not unwrapped.
|
|
176
|
+
if (
|
|
177
|
+
callback.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
178
|
+
callback.async ||
|
|
179
|
+
!Arr.isFixedLengthTuple(1, callback.params)
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const param = callback.params[0];
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
param.type !== AST_NODE_TYPES.Identifier &&
|
|
188
|
+
param.type !== AST_NODE_TYPES.ObjectPattern &&
|
|
189
|
+
param.type !== AST_NODE_TYPES.ArrayPattern
|
|
190
|
+
) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// `(x?: T) => …` may be called without the element.
|
|
195
|
+
if (param.optional) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const typeArguments = node.typeArguments;
|
|
200
|
+
|
|
201
|
+
// `flatMap<U, This>` has no counterpart for `This`.
|
|
202
|
+
if (typeArguments !== undefined && typeArguments.params.length !== 1) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const shape = getFilterMapShape(sourceCode, callback);
|
|
207
|
+
|
|
208
|
+
if (shape === undefined) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const receiver = callee.object;
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
(shape.prelude ?? []).some((statement) =>
|
|
216
|
+
someDescendant(sourceCode, statement, isReturnStatement, false),
|
|
217
|
+
) ||
|
|
218
|
+
someDescendant(sourceCode, callback, rebindsInGenerator, true) ||
|
|
219
|
+
someDescendant(sourceCode, receiver, rebindsInGenerator, true)
|
|
220
|
+
) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const paramNames = new Set(
|
|
225
|
+
sourceCode
|
|
226
|
+
.getDeclaredVariables(callback)
|
|
227
|
+
.map((variable) => variable.name),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
someDescendant(
|
|
232
|
+
sourceCode,
|
|
233
|
+
receiver,
|
|
234
|
+
(n) =>
|
|
235
|
+
n.type === AST_NODE_TYPES.Identifier &&
|
|
236
|
+
paramNames.has(n.name) &&
|
|
237
|
+
isReference(n) &&
|
|
238
|
+
!isBoundWithin(sourceCode, n, receiver),
|
|
239
|
+
true,
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// At every layer of type wrappers (see typeWrapperLayers).
|
|
246
|
+
const isArrayReceiver = getLayerTypes(receiver).every((type) =>
|
|
247
|
+
isArrayOrTupleType(checker, type),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!isArrayReceiver) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const replacement = buildReplacement({
|
|
255
|
+
sourceCode,
|
|
256
|
+
negationContext,
|
|
257
|
+
node,
|
|
258
|
+
arrName,
|
|
259
|
+
typeArgumentsText:
|
|
260
|
+
typeArguments === undefined
|
|
261
|
+
? ''
|
|
262
|
+
: sourceCode.getText(typeArguments),
|
|
263
|
+
// The loop variable cannot carry the annotation; the suggestion
|
|
264
|
+
// leaves it to the receiver's element type.
|
|
265
|
+
paramText:
|
|
266
|
+
param.typeAnnotation === undefined
|
|
267
|
+
? sourceCode.getText(param)
|
|
268
|
+
: sourceCode.text
|
|
269
|
+
.slice(param.range[0], param.typeAnnotation.range[0])
|
|
270
|
+
.trimEnd(),
|
|
271
|
+
receiver,
|
|
272
|
+
shape,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const autofixable =
|
|
276
|
+
!shape.wrapped &&
|
|
277
|
+
param.typeAnnotation === undefined &&
|
|
278
|
+
callback.returnType === undefined &&
|
|
279
|
+
!isFilterCall(receiver) &&
|
|
280
|
+
!someDescendant(
|
|
281
|
+
sourceCode,
|
|
282
|
+
receiver,
|
|
283
|
+
(n) =>
|
|
284
|
+
isNarrowedHere(
|
|
285
|
+
sourceCode,
|
|
286
|
+
checker,
|
|
287
|
+
getTsNode,
|
|
288
|
+
n,
|
|
289
|
+
n === skipTypeWrappers(receiver),
|
|
290
|
+
),
|
|
291
|
+
true,
|
|
292
|
+
) &&
|
|
293
|
+
!losesComments(sourceCode, callback, shape) &&
|
|
294
|
+
!needsMutableResult(sourceCode, checker, getTsNode, node);
|
|
295
|
+
|
|
296
|
+
mut_matches.push({ node, replacement, autofixable });
|
|
297
|
+
},
|
|
298
|
+
'Program:exit': () => {
|
|
299
|
+
// The import is added by the first fix only, so that several fixes do
|
|
300
|
+
// not insert it at the same position. When the first match is not
|
|
301
|
+
// autofixable, the first autofixable one carries it instead.
|
|
302
|
+
const firstAutofixable = mut_matches.find((m) => m.autofixable);
|
|
303
|
+
|
|
304
|
+
for (const match of mut_matches) {
|
|
305
|
+
const buildFix =
|
|
306
|
+
(withImport: boolean): TSESLint.ReportFixFunction =>
|
|
307
|
+
(fixer) => [
|
|
308
|
+
...(withImport && arrLocalName === undefined
|
|
309
|
+
? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
|
|
310
|
+
'Arr',
|
|
311
|
+
])
|
|
312
|
+
: []),
|
|
313
|
+
fixer.replaceText(match.node, match.replacement),
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
if (match.autofixable) {
|
|
317
|
+
context.report({
|
|
318
|
+
node: match.node,
|
|
319
|
+
messageId: 'useArrGenerate',
|
|
320
|
+
fix: buildFix(match === firstAutofixable),
|
|
321
|
+
});
|
|
322
|
+
} else {
|
|
323
|
+
context.report({
|
|
324
|
+
node: match.node,
|
|
325
|
+
messageId: 'useArrGenerate',
|
|
326
|
+
suggest: [
|
|
327
|
+
{
|
|
328
|
+
messageId: 'suggestArrGenerate',
|
|
329
|
+
fix: buildFix(true),
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
defaultOptions: [],
|
|
339
|
+
} as const;
|
|
340
|
+
|
|
341
|
+
/** The parts of a filter-and-map callback that the rewrite keeps. */
|
|
342
|
+
type FilterMapShape = Readonly<{
|
|
343
|
+
/** The statements before the `return` of a block body. */
|
|
344
|
+
prelude: readonly TSESTree.Statement[] | undefined;
|
|
345
|
+
/**
|
|
346
|
+
* The source of a block body from just inside its `{` up to its `return`:
|
|
347
|
+
* the statements before it, and every comment among or above them.
|
|
348
|
+
*/
|
|
349
|
+
preludeRange: FixedLengthTuple<2, number> | undefined;
|
|
350
|
+
test: TSESTree.Expression;
|
|
351
|
+
/** Whether the kept branch is the alternate (`cond ? [] : [x]`). */
|
|
352
|
+
negated: boolean;
|
|
353
|
+
elements: readonly TSESTree.Expression[];
|
|
354
|
+
/** Whether a type wrapper surrounds the conditional or either branch. */
|
|
355
|
+
wrapped: boolean;
|
|
356
|
+
}>;
|
|
357
|
+
|
|
358
|
+
const getFilterMapShape = (
|
|
359
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
360
|
+
sourceCode: TSESLint.SourceCode,
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
362
|
+
callback: TSESTree.ArrowFunctionExpression,
|
|
363
|
+
): FilterMapShape | undefined => {
|
|
364
|
+
const { body } = callback;
|
|
365
|
+
|
|
366
|
+
const returnStatement =
|
|
367
|
+
body.type === AST_NODE_TYPES.BlockStatement ? body.body.at(-1) : undefined;
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
body.type === AST_NODE_TYPES.BlockStatement &&
|
|
371
|
+
(returnStatement?.type !== AST_NODE_TYPES.ReturnStatement ||
|
|
372
|
+
returnStatement.argument === null)
|
|
373
|
+
) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const returned =
|
|
378
|
+
body.type === AST_NODE_TYPES.BlockStatement
|
|
379
|
+
? // eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
380
|
+
(returnStatement as TSESTree.ReturnStatement).argument
|
|
381
|
+
: body;
|
|
382
|
+
|
|
383
|
+
if (returned === null) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const conditional = skipTypeWrappers(returned);
|
|
388
|
+
|
|
389
|
+
if (conditional.type !== AST_NODE_TYPES.ConditionalExpression) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const consequent = skipTypeWrappers(conditional.consequent);
|
|
394
|
+
|
|
395
|
+
const alternate = skipTypeWrappers(conditional.alternate);
|
|
396
|
+
|
|
397
|
+
if (
|
|
398
|
+
consequent.type !== AST_NODE_TYPES.ArrayExpression ||
|
|
399
|
+
alternate.type !== AST_NODE_TYPES.ArrayExpression
|
|
400
|
+
) {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const negated = Arr.isEmpty(consequent.elements);
|
|
405
|
+
|
|
406
|
+
const kept = negated ? alternate : consequent;
|
|
407
|
+
|
|
408
|
+
const dropped = negated ? consequent : alternate;
|
|
409
|
+
|
|
410
|
+
if (!Arr.isEmpty(dropped.elements) || Arr.isEmpty(kept.elements)) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const elements = kept.elements.filter(
|
|
415
|
+
(element): element is TSESTree.Expression =>
|
|
416
|
+
element !== null && element.type !== AST_NODE_TYPES.SpreadElement,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (elements.length !== kept.elements.length) {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const openingBrace =
|
|
424
|
+
body.type === AST_NODE_TYPES.BlockStatement
|
|
425
|
+
? sourceCode.getFirstToken(body)
|
|
426
|
+
: null;
|
|
427
|
+
|
|
428
|
+
const preludeStart =
|
|
429
|
+
openingBrace === null
|
|
430
|
+
? undefined
|
|
431
|
+
: sourceCode.getTokenAfter(openingBrace, { includeComments: true })
|
|
432
|
+
?.range[0];
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
prelude:
|
|
436
|
+
body.type === AST_NODE_TYPES.BlockStatement
|
|
437
|
+
? body.body.slice(0, -1)
|
|
438
|
+
: undefined,
|
|
439
|
+
preludeRange:
|
|
440
|
+
preludeStart === undefined ||
|
|
441
|
+
returnStatement === undefined ||
|
|
442
|
+
preludeStart >= returnStatement.range[0]
|
|
443
|
+
? undefined
|
|
444
|
+
: [preludeStart, returnStatement.range[0]],
|
|
445
|
+
test: conditional.test,
|
|
446
|
+
negated,
|
|
447
|
+
elements,
|
|
448
|
+
wrapped:
|
|
449
|
+
conditional !== returned ||
|
|
450
|
+
consequent !== conditional.consequent ||
|
|
451
|
+
alternate !== conditional.alternate,
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
456
|
+
const buildReplacement = ({
|
|
457
|
+
sourceCode,
|
|
458
|
+
negationContext,
|
|
459
|
+
node,
|
|
460
|
+
arrName,
|
|
461
|
+
typeArgumentsText,
|
|
462
|
+
paramText,
|
|
463
|
+
receiver,
|
|
464
|
+
shape,
|
|
465
|
+
}: Readonly<{
|
|
466
|
+
sourceCode: TSESLint.SourceCode;
|
|
467
|
+
negationContext: NegationContext;
|
|
468
|
+
node: TSESTree.CallExpression;
|
|
469
|
+
arrName: string;
|
|
470
|
+
typeArgumentsText: string;
|
|
471
|
+
paramText: string;
|
|
472
|
+
receiver: TSESTree.Expression;
|
|
473
|
+
shape: FilterMapShape;
|
|
474
|
+
}>): string => {
|
|
475
|
+
const indent0 = lineIndentOf(sourceCode, node.range[0]);
|
|
476
|
+
|
|
477
|
+
const indent1 = `${indent0} ` as const;
|
|
478
|
+
|
|
479
|
+
const indent2 = `${indent1} ` as const;
|
|
480
|
+
|
|
481
|
+
const indent3 = `${indent2} ` as const;
|
|
482
|
+
|
|
483
|
+
const condition = reindent(
|
|
484
|
+
sourceCode,
|
|
485
|
+
shape.test.range,
|
|
486
|
+
indent2,
|
|
487
|
+
shape.negated
|
|
488
|
+
? (negationOf(shape.test, negationContext, undefined) ??
|
|
489
|
+
prefixedNegationOf(shape.test, sourceCode))
|
|
490
|
+
: sourceCode.getText(shape.test),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const { preludeRange } = shape;
|
|
494
|
+
|
|
495
|
+
const preludeLines =
|
|
496
|
+
preludeRange === undefined
|
|
497
|
+
? ([] as const)
|
|
498
|
+
: ([
|
|
499
|
+
`${indent2}${reindent(
|
|
500
|
+
sourceCode,
|
|
501
|
+
preludeRange,
|
|
502
|
+
indent2,
|
|
503
|
+
sourceCode.text.slice(...preludeRange).trimEnd(),
|
|
504
|
+
)}`,
|
|
505
|
+
] as const);
|
|
506
|
+
|
|
507
|
+
const yieldLines = shape.elements.map(
|
|
508
|
+
(element) =>
|
|
509
|
+
`${indent3}yield ${reindent(
|
|
510
|
+
sourceCode,
|
|
511
|
+
element.range,
|
|
512
|
+
indent3,
|
|
513
|
+
getArgumentText(sourceCode, element),
|
|
514
|
+
)};`,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const receiverText = reindent(
|
|
518
|
+
sourceCode,
|
|
519
|
+
receiver.range,
|
|
520
|
+
indent1,
|
|
521
|
+
getArgumentText(sourceCode, receiver),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
return [
|
|
525
|
+
`${arrName}.generate${typeArgumentsText}(function* () {`,
|
|
526
|
+
`${indent1}for (const ${paramText} of ${receiverText}) {`,
|
|
527
|
+
...preludeLines,
|
|
528
|
+
`${indent2}if (${condition}) {`,
|
|
529
|
+
...yieldLines,
|
|
530
|
+
`${indent2}}`,
|
|
531
|
+
`${indent1}}`,
|
|
532
|
+
`${indent0}})`,
|
|
533
|
+
].join('\n');
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Whether a comment inside `callback` lies outside every piece of source the
|
|
538
|
+
* rewrite carries over, and would therefore be dropped.
|
|
539
|
+
*/
|
|
540
|
+
const losesComments = (
|
|
541
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
542
|
+
sourceCode: TSESLint.SourceCode,
|
|
543
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
544
|
+
callback: TSESTree.ArrowFunctionExpression,
|
|
545
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
546
|
+
shape: FilterMapShape,
|
|
547
|
+
): boolean => {
|
|
548
|
+
const keptRanges: readonly FixedLengthTuple<2, number>[] = [
|
|
549
|
+
shape.test.range,
|
|
550
|
+
...shape.elements.map((element) => element.range),
|
|
551
|
+
...(shape.preludeRange === undefined
|
|
552
|
+
? ([] as const)
|
|
553
|
+
: ([shape.preludeRange] as const)),
|
|
554
|
+
] as const;
|
|
555
|
+
|
|
556
|
+
return sourceCode
|
|
557
|
+
.getCommentsInside(callback)
|
|
558
|
+
.some((comment) =>
|
|
559
|
+
keptRanges.every(
|
|
560
|
+
([start, end]) =>
|
|
561
|
+
!(start <= comment.range[0] && comment.range[1] <= end),
|
|
562
|
+
),
|
|
563
|
+
);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* `text`, the source at `range`, with every line after the first shifted from
|
|
568
|
+
* the indentation of the line `range` starts on to `targetIndent`. Left as it
|
|
569
|
+
* is when a template literal or string spans lines inside it, since shifting
|
|
570
|
+
* those lines would change the string.
|
|
571
|
+
*/
|
|
572
|
+
const reindent = (
|
|
573
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
574
|
+
sourceCode: TSESLint.SourceCode,
|
|
575
|
+
range: FixedLengthTuple<2, number>,
|
|
576
|
+
targetIndent: string,
|
|
577
|
+
text: string,
|
|
578
|
+
): string => {
|
|
579
|
+
if (!text.includes('\n')) {
|
|
580
|
+
return text;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const spansLines = sourceCode.ast.tokens.some(
|
|
584
|
+
(token) =>
|
|
585
|
+
range[0] <= token.range[0] &&
|
|
586
|
+
token.range[1] <= range[1] &&
|
|
587
|
+
(token.type === AST_TOKEN_TYPES.Template ||
|
|
588
|
+
token.type === AST_TOKEN_TYPES.String) &&
|
|
589
|
+
token.loc.start.line !== token.loc.end.line,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
if (spansLines) {
|
|
593
|
+
return text;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const sourceIndent = lineIndentOf(sourceCode, range[0]);
|
|
597
|
+
|
|
598
|
+
const [first, ...rest] = text.split('\n');
|
|
599
|
+
|
|
600
|
+
return [
|
|
601
|
+
first,
|
|
602
|
+
...rest.map((line) =>
|
|
603
|
+
line.startsWith(sourceIndent)
|
|
604
|
+
? `${targetIndent}${line.slice(sourceIndent.length)}`
|
|
605
|
+
: line,
|
|
606
|
+
),
|
|
607
|
+
].join('\n');
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/** The whitespace at the start of the line that `index` is on. */
|
|
611
|
+
const lineIndentOf = (
|
|
612
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
613
|
+
sourceCode: TSESLint.SourceCode,
|
|
614
|
+
index: number,
|
|
615
|
+
): string => {
|
|
616
|
+
const { line } = sourceCode.getLocFromIndex(index);
|
|
617
|
+
|
|
618
|
+
const lineText = sourceCode.lines[line - 1] ?? '';
|
|
619
|
+
|
|
620
|
+
return /^\s*/u.exec(lineText)?.[0] ?? '';
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
/** Whether `node`, with its type wrappers taken off, is a `….filter(…)` call. */
|
|
624
|
+
const isFilterCall = (
|
|
625
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
626
|
+
node: TSESTree.Expression,
|
|
627
|
+
): boolean => {
|
|
628
|
+
const call = skipTypeWrappers(node);
|
|
629
|
+
|
|
630
|
+
if (call.type !== AST_NODE_TYPES.CallExpression) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const callee = skipTypeWrappers(call.callee);
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
638
|
+
!callee.computed &&
|
|
639
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
640
|
+
callee.property.name === 'filter'
|
|
641
|
+
);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Whether `node`, a reference or a member access, has a type here narrower
|
|
646
|
+
* than its declared one, by a narrowing that a function created here would not
|
|
647
|
+
* see. TypeScript keeps the narrowing of a `const`, and of a parameter or `let`
|
|
648
|
+
* that is never reassigned, inside a closure; it drops every other one,
|
|
649
|
+
* including any narrowing of a property.
|
|
650
|
+
*/
|
|
651
|
+
const isNarrowedHere = (
|
|
652
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
653
|
+
sourceCode: TSESLint.SourceCode,
|
|
654
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
655
|
+
checker: ts.TypeChecker,
|
|
656
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
657
|
+
getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
|
|
658
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
659
|
+
node: TSESTree.Node,
|
|
660
|
+
isReceiver: boolean,
|
|
661
|
+
): boolean => {
|
|
662
|
+
if (
|
|
663
|
+
node.type !== AST_NODE_TYPES.Identifier &&
|
|
664
|
+
node.type !== AST_NODE_TYPES.MemberExpression
|
|
665
|
+
) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// A method about to be called (`xs.flat()`): its type is that of the method,
|
|
670
|
+
// which no narrowing of interest changes.
|
|
671
|
+
if (
|
|
672
|
+
node.parent.type === AST_NODE_TYPES.CallExpression &&
|
|
673
|
+
node.parent.callee === node
|
|
674
|
+
) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
679
|
+
if (!isReference(node)) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
|
|
684
|
+
|
|
685
|
+
if (
|
|
686
|
+
variable?.references.every(
|
|
687
|
+
(reference) => !reference.isWrite() || reference.init === true,
|
|
688
|
+
) === true
|
|
689
|
+
) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const tsNode = getTsNode(node);
|
|
695
|
+
|
|
696
|
+
if (tsNode === undefined) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const declaredType = getDeclaredType(checker, tsNode);
|
|
701
|
+
|
|
702
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
declaredType !== undefined &&
|
|
706
|
+
declaredType !== type &&
|
|
707
|
+
// The loop only iterates the receiver itself, so a narrowing that leaves
|
|
708
|
+
// it an array of the same elements (`Arr.isNonEmpty(xs)`) loses nothing.
|
|
709
|
+
!(isReceiver && iteratesTheSame(checker, declaredType, type))
|
|
710
|
+
);
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Whether two types are both arrays or tuples whose elements have one type,
|
|
715
|
+
* as the declared and the narrowed type of `xs` after `Arr.isNonEmpty(xs)`
|
|
716
|
+
* are.
|
|
717
|
+
*/
|
|
718
|
+
const iteratesTheSame = (
|
|
719
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
720
|
+
checker: ts.TypeChecker,
|
|
721
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
722
|
+
a: ts.Type,
|
|
723
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
724
|
+
b: ts.Type,
|
|
725
|
+
): boolean => {
|
|
726
|
+
if (!isArrayOrTupleType(checker, a) || !isArrayOrTupleType(checker, b)) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const elementOfA = checker.getIndexTypeOfType(a, ts.IndexKind.Number);
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
elementOfA !== undefined &&
|
|
734
|
+
elementOfA === checker.getIndexTypeOfType(b, ts.IndexKind.Number)
|
|
735
|
+
);
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* The type of a variable or a property as declared, before any narrowing, or
|
|
740
|
+
* `undefined` when there is none to compare with (an index read with no
|
|
741
|
+
* property behind it, which nothing narrows).
|
|
742
|
+
*
|
|
743
|
+
* A property's is read off the type of the object it is accessed on, which
|
|
744
|
+
* also covers a property of a mapped type (`Readonly<…>`), whose symbol has no
|
|
745
|
+
* declaration of its own. A variable's is the type at its declaration
|
|
746
|
+
* (`checker.getTypeOfSymbol` would say the same, but is missing before
|
|
747
|
+
* TypeScript 5.1).
|
|
748
|
+
*/
|
|
749
|
+
const getDeclaredType = (
|
|
750
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
751
|
+
checker: ts.TypeChecker,
|
|
752
|
+
|
|
753
|
+
tsNode: ts.Node,
|
|
754
|
+
): ts.Type | undefined => {
|
|
755
|
+
if (
|
|
756
|
+
ts.isPropertyAccessExpression(tsNode) ||
|
|
757
|
+
ts.isElementAccessExpression(tsNode)
|
|
758
|
+
) {
|
|
759
|
+
const name = ts.isPropertyAccessExpression(tsNode)
|
|
760
|
+
? tsNode.name.text
|
|
761
|
+
: ts.isStringLiteralLike(tsNode.argumentExpression) ||
|
|
762
|
+
ts.isNumericLiteral(tsNode.argumentExpression)
|
|
763
|
+
? tsNode.argumentExpression.text
|
|
764
|
+
: undefined;
|
|
765
|
+
|
|
766
|
+
const property =
|
|
767
|
+
name === undefined
|
|
768
|
+
? undefined
|
|
769
|
+
: checker.getPropertyOfType(
|
|
770
|
+
checker.getTypeAtLocation(tsNode.expression),
|
|
771
|
+
name,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// Asked at the object rather than at the access, so that no narrowing of
|
|
775
|
+
// the access applies.
|
|
776
|
+
return property === undefined
|
|
777
|
+
? undefined
|
|
778
|
+
: checker.getTypeOfSymbolAtLocation(property, tsNode.expression);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const symbol = checker.getSymbolAtLocation(tsNode);
|
|
782
|
+
|
|
783
|
+
const declaration = symbol?.valueDeclaration;
|
|
784
|
+
|
|
785
|
+
return symbol === undefined || declaration === undefined
|
|
786
|
+
? undefined
|
|
787
|
+
: checker.getTypeOfSymbolAtLocation(symbol, declaration);
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Whether the binding `identifier` reads is declared inside `container`, as
|
|
792
|
+
* the parameter of an arrow function in the receiver is: the loop variable
|
|
793
|
+
* does not shadow it.
|
|
794
|
+
*/
|
|
795
|
+
const isBoundWithin = (
|
|
796
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
797
|
+
sourceCode: TSESLint.SourceCode,
|
|
798
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
799
|
+
identifier: TSESTree.Identifier,
|
|
800
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
801
|
+
container: TSESTree.Node,
|
|
802
|
+
): boolean => {
|
|
803
|
+
const variable = ASTUtils.findVariable(
|
|
804
|
+
sourceCode.getScope(identifier),
|
|
805
|
+
identifier,
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
return (
|
|
809
|
+
variable !== null &&
|
|
810
|
+
Arr.isNonEmpty(variable.defs) &&
|
|
811
|
+
variable.defs.every(
|
|
812
|
+
(def) =>
|
|
813
|
+
container.range[0] <= def.name.range[0] &&
|
|
814
|
+
def.name.range[1] <= container.range[1],
|
|
815
|
+
)
|
|
816
|
+
);
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
/** Whether an identifier reads a binding, rather than naming a property or a key. */
|
|
820
|
+
const isReference = (
|
|
821
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
822
|
+
node: TSESTree.Identifier,
|
|
823
|
+
): boolean => {
|
|
824
|
+
const { parent } = node;
|
|
825
|
+
|
|
826
|
+
return !(
|
|
827
|
+
(parent.type === AST_NODE_TYPES.MemberExpression &&
|
|
828
|
+
parent.property === node &&
|
|
829
|
+
!parent.computed) ||
|
|
830
|
+
(parent.type === AST_NODE_TYPES.Property &&
|
|
831
|
+
parent.key === node &&
|
|
832
|
+
!parent.computed &&
|
|
833
|
+
!parent.shorthand)
|
|
834
|
+
);
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Whether `node` or a node inside it satisfies `predicate`. A nested
|
|
839
|
+
* non-arrow function or class has its own `this`, `arguments` and `return`,
|
|
840
|
+
* and is not entered; a nested arrow function shares `this` and `arguments`,
|
|
841
|
+
* and is entered only when `enterArrows` is set.
|
|
842
|
+
*/
|
|
843
|
+
const someDescendant = (
|
|
844
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
845
|
+
sourceCode: TSESLint.SourceCode,
|
|
846
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
847
|
+
node: TSESTree.Node,
|
|
848
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
849
|
+
predicate: (n: TSESTree.Node) => boolean,
|
|
850
|
+
enterArrows: boolean,
|
|
851
|
+
isRoot: boolean = true,
|
|
852
|
+
): boolean => {
|
|
853
|
+
if (predicate(node)) {
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (
|
|
858
|
+
!isRoot &&
|
|
859
|
+
(node.type === AST_NODE_TYPES.FunctionDeclaration ||
|
|
860
|
+
node.type === AST_NODE_TYPES.FunctionExpression ||
|
|
861
|
+
node.type === AST_NODE_TYPES.ClassBody ||
|
|
862
|
+
(!enterArrows && node.type === AST_NODE_TYPES.ArrowFunctionExpression))
|
|
863
|
+
) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const keys = sourceCode.visitorKeys[node.type] ?? [];
|
|
868
|
+
|
|
869
|
+
return keys.some((key) => {
|
|
870
|
+
const child: unknown = isRecord(node) ? node[key] : undefined;
|
|
871
|
+
|
|
872
|
+
return (Arr.isArray(child) ? child : [child]).some(
|
|
873
|
+
(c) =>
|
|
874
|
+
isNode(c) &&
|
|
875
|
+
someDescendant(sourceCode, c, predicate, enterArrows, false),
|
|
876
|
+
);
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const isNode = (value: unknown): value is TSESTree.Node =>
|
|
881
|
+
isRecord(value) && hasKey(value, 'type') && typeof value.type === 'string';
|
|
882
|
+
|
|
883
|
+
const isReturnStatement = (
|
|
884
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
885
|
+
node: TSESTree.Node,
|
|
886
|
+
): boolean => node.type === AST_NODE_TYPES.ReturnStatement;
|
|
887
|
+
|
|
888
|
+
/** What a generator function binds anew: `this`, `arguments`, `super`, `new.target`. */
|
|
889
|
+
const rebindsInGenerator = (
|
|
890
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
891
|
+
node: TSESTree.Node,
|
|
892
|
+
): boolean =>
|
|
893
|
+
node.type === AST_NODE_TYPES.ThisExpression ||
|
|
894
|
+
node.type === AST_NODE_TYPES.Super ||
|
|
895
|
+
node.type === AST_NODE_TYPES.MetaProperty ||
|
|
896
|
+
(node.type === AST_NODE_TYPES.Identifier && node.name === 'arguments');
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Whether `type` is an array or tuple, or a union of them, or an intersection
|
|
900
|
+
* with one in it (`readonly T[] & Brand`).
|
|
901
|
+
*/
|
|
902
|
+
const isArrayOrTupleType = (
|
|
903
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
904
|
+
checker: ts.TypeChecker,
|
|
905
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
906
|
+
type: ts.Type,
|
|
907
|
+
): boolean => {
|
|
908
|
+
const resolved =
|
|
909
|
+
(type.flags & ts.TypeFlags.TypeParameter) !== 0
|
|
910
|
+
? (checker.getBaseConstraintOfType(type) ?? type)
|
|
911
|
+
: type;
|
|
912
|
+
|
|
913
|
+
return resolved.isUnion()
|
|
914
|
+
? resolved.types.every((t) => isArrayOrTupleType(checker, t))
|
|
915
|
+
: resolved.isIntersection()
|
|
916
|
+
? resolved.types.some((t) => isArrayOrTupleType(checker, t))
|
|
917
|
+
: checker.isArrayType(resolved) || checker.isTupleType(resolved);
|
|
918
|
+
};
|