@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.1
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 -0
- package/lib/index.js +64 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
- package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
- package/lib/rules/enforce-cloud-function-id-length.js +104 -0
- package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
- package/lib/rules/enforce-is-prefix-validators.js +304 -0
- package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
- package/lib/rules/enforce-m3-sentence-case.js +430 -0
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
- package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
- package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
- package/lib/rules/enforce-types-directory-placement.js +276 -0
- package/lib/rules/no-direct-function-state.d.ts +8 -0
- package/lib/rules/no-direct-function-state.js +285 -0
- package/lib/rules/no-fill-template-mutation.d.ts +1 -0
- package/lib/rules/no-fill-template-mutation.js +324 -0
- package/lib/rules/no-hungarian.js +18 -26
- package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
- package/lib/rules/no-portal-inside-tooltip.js +219 -0
- package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
- package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
- package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
- package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
- package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
- package/lib/rules/no-stablehash-react-nodes.js +325 -0
- package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
- package/lib/rules/parallelize-loop-awaits.js +582 -0
- package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
- package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
- package/lib/rules/prefer-spread-over-reassembly.js +401 -0
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
- package/lib/rules/prefer-use-base62-id.d.ts +8 -0
- package/lib/rules/prefer-use-base62-id.js +483 -0
- package/lib/rules/prefer-use-theme.d.ts +1 -0
- package/lib/rules/prefer-use-theme.js +206 -0
- package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
- package/lib/rules/prefer-utility-function-own-file.js +505 -0
- package/lib/rules/react-memoize-literals.js +106 -1
- package/lib/rules/require-props-composition.d.ts +10 -0
- package/lib/rules/require-props-composition.js +433 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
- package/package.json +1 -1
- package/release-manifest.json +212 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parallelizeLoopAwaits = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_COORDINATOR_PATTERNS = [
|
|
7
|
+
'batchManager',
|
|
8
|
+
'batch',
|
|
9
|
+
'transaction',
|
|
10
|
+
'collector',
|
|
11
|
+
'accumulator',
|
|
12
|
+
'aggregator',
|
|
13
|
+
'mutex',
|
|
14
|
+
'lock',
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_RATE_LIMITED_PATTERNS = [
|
|
17
|
+
'sleep',
|
|
18
|
+
'delay',
|
|
19
|
+
'throttle',
|
|
20
|
+
'rateLimit',
|
|
21
|
+
];
|
|
22
|
+
const defaultOptions = [
|
|
23
|
+
{
|
|
24
|
+
coordinatorPatterns: DEFAULT_COORDINATOR_PATTERNS,
|
|
25
|
+
rateLimitedPatterns: DEFAULT_RATE_LIMITED_PATTERNS,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
29
|
+
name: 'parallelize-loop-awaits',
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'suggestion',
|
|
32
|
+
docs: {
|
|
33
|
+
description: 'Disallow sequential await expressions inside loops when iterations could be parallelized with Promise.all(items.map(...))',
|
|
34
|
+
recommended: 'error',
|
|
35
|
+
},
|
|
36
|
+
fixable: undefined,
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
coordinatorPatterns: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'string' },
|
|
44
|
+
default: DEFAULT_COORDINATOR_PATTERNS,
|
|
45
|
+
},
|
|
46
|
+
rateLimitedPatterns: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
items: { type: 'string' },
|
|
49
|
+
default: DEFAULT_RATE_LIMITED_PATTERNS,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
messages: {
|
|
56
|
+
parallelizeLoopAwaits: 'Sequential await in loop can likely be parallelized using Promise.all(items.map(...)). If sequential execution is intentional, add an `// eslint-disable-next-line @blumintinc/blumint/parallelize-loop-awaits -- <reason>` comment explaining why.',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
defaultOptions,
|
|
60
|
+
create(context, [options]) {
|
|
61
|
+
const coordinatorPatterns = options?.coordinatorPatterns ?? DEFAULT_COORDINATOR_PATTERNS;
|
|
62
|
+
const rateLimitedPatterns = options?.rateLimitedPatterns ?? DEFAULT_RATE_LIMITED_PATTERNS;
|
|
63
|
+
/**
|
|
64
|
+
* Tests whether an identifier name matches a coordinator/rate-limit pattern.
|
|
65
|
+
* Uses camelCase-aware matching: the identifier equals the pattern, starts
|
|
66
|
+
* with it, or ends with it (all case-insensitive). This avoids false
|
|
67
|
+
* positives from substring coincidences like "nonBatchMethod" matching "batch".
|
|
68
|
+
*/
|
|
69
|
+
function matchesPattern(identifierName, pattern) {
|
|
70
|
+
const lower = identifierName.toLowerCase();
|
|
71
|
+
const pat = pattern.toLowerCase();
|
|
72
|
+
return lower === pat || lower.startsWith(pat) || lower.endsWith(pat);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Recursively collects all identifier names referenced in a node.
|
|
76
|
+
* Does not cross nested function boundaries.
|
|
77
|
+
*/
|
|
78
|
+
function collectIdentifiers(node, names, stopAtFunctions = false) {
|
|
79
|
+
if (stopAtFunctions && node.type !== node.type)
|
|
80
|
+
return; // no-op placeholder
|
|
81
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
82
|
+
names.add(node.name);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Do not descend into nested functions when stopAtFunctions is set
|
|
86
|
+
if (stopAtFunctions &&
|
|
87
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
88
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
89
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const key in node) {
|
|
93
|
+
if (key === 'parent' ||
|
|
94
|
+
key === 'range' ||
|
|
95
|
+
key === 'loc' ||
|
|
96
|
+
key === 'type')
|
|
97
|
+
continue;
|
|
98
|
+
const child = node[key];
|
|
99
|
+
if (child && typeof child === 'object') {
|
|
100
|
+
if (Array.isArray(child)) {
|
|
101
|
+
for (const item of child) {
|
|
102
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
103
|
+
collectIdentifiers(item, names, stopAtFunctions);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if ('type' in child) {
|
|
108
|
+
collectIdentifiers(child, names, stopAtFunctions);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns the function call name(s) from an await expression, used to
|
|
115
|
+
* check for rate-limiting function calls like `await sleep(1000)`.
|
|
116
|
+
*/
|
|
117
|
+
function getCallNames(awaitExpr) {
|
|
118
|
+
const names = [];
|
|
119
|
+
const { argument } = awaitExpr;
|
|
120
|
+
let callExpr = null;
|
|
121
|
+
if (argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
122
|
+
callExpr = argument;
|
|
123
|
+
}
|
|
124
|
+
else if (argument.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
125
|
+
argument.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
126
|
+
callExpr = argument.expression;
|
|
127
|
+
}
|
|
128
|
+
if (!callExpr)
|
|
129
|
+
return names;
|
|
130
|
+
const { callee } = callExpr;
|
|
131
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
132
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
133
|
+
names.push(callee.property.name);
|
|
134
|
+
}
|
|
135
|
+
else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
136
|
+
names.push(callee.name);
|
|
137
|
+
}
|
|
138
|
+
return names;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Determines whether a node (a loop body) directly contains a break,
|
|
142
|
+
* continue, or return statement that is NOT inside a nested function.
|
|
143
|
+
* These statements indicate the loop's control flow depends on the async
|
|
144
|
+
* result and cannot be safely parallelized.
|
|
145
|
+
*/
|
|
146
|
+
function containsBreakContinueReturn(node) {
|
|
147
|
+
if (node.type === utils_1.AST_NODE_TYPES.BreakStatement ||
|
|
148
|
+
node.type === utils_1.AST_NODE_TYPES.ContinueStatement ||
|
|
149
|
+
node.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
// Don't descend into nested functions — their control flow is separate
|
|
153
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
154
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
155
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
for (const key in node) {
|
|
159
|
+
if (key === 'parent' ||
|
|
160
|
+
key === 'range' ||
|
|
161
|
+
key === 'loc' ||
|
|
162
|
+
key === 'type')
|
|
163
|
+
continue;
|
|
164
|
+
const child = node[key];
|
|
165
|
+
if (child && typeof child === 'object') {
|
|
166
|
+
if (Array.isArray(child)) {
|
|
167
|
+
for (const item of child) {
|
|
168
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
169
|
+
if (containsBreakContinueReturn(item))
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if ('type' in child) {
|
|
175
|
+
if (containsBreakContinueReturn(child))
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Returns true when the await expression is directly wrapped in a try
|
|
184
|
+
* block within the same loop body. This indicates per-iteration error
|
|
185
|
+
* handling is in use, where sequential semantics are intentional.
|
|
186
|
+
*/
|
|
187
|
+
function isAwaitInTryCatch(awaitNode, loopNode) {
|
|
188
|
+
let current = awaitNode.parent;
|
|
189
|
+
while (current && current !== loopNode) {
|
|
190
|
+
if (current.type === utils_1.AST_NODE_TYPES.TryStatement) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
current = current.parent;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Checks whether any identifier used in the loop body matches any of the
|
|
199
|
+
* coordinator patterns (using camelCase-aware matching), indicating shared
|
|
200
|
+
* mutable state that requires sequential execution (e.g. BatchManager,
|
|
201
|
+
* Firestore Transaction).
|
|
202
|
+
*/
|
|
203
|
+
function hasCoordinatorInBody(body) {
|
|
204
|
+
const ids = new Set();
|
|
205
|
+
collectIdentifiers(body, ids, true);
|
|
206
|
+
for (const id of ids) {
|
|
207
|
+
for (const pattern of coordinatorPatterns) {
|
|
208
|
+
if (matchesPattern(id, pattern)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Checks whether the loop body contains a call to a rate-limiting
|
|
217
|
+
* function (e.g. sleep, delay, throttle). When present, sequential
|
|
218
|
+
* execution is almost certainly intentional.
|
|
219
|
+
*
|
|
220
|
+
* Uses exact case-insensitive matching (not substring) to avoid
|
|
221
|
+
* false positives.
|
|
222
|
+
*/
|
|
223
|
+
function hasRateLimitedCallInBody(body) {
|
|
224
|
+
const ids = new Set();
|
|
225
|
+
collectIdentifiers(body, ids, true);
|
|
226
|
+
for (const id of ids) {
|
|
227
|
+
for (const pattern of rateLimitedPatterns) {
|
|
228
|
+
if (id.toLowerCase() === pattern.toLowerCase()) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Collects all variables declared INSIDE the loop body (at the direct
|
|
237
|
+
* body scope, not crossing nested function boundaries). These are
|
|
238
|
+
* iteration-local variables.
|
|
239
|
+
*/
|
|
240
|
+
function collectLoopLocalVars(body) {
|
|
241
|
+
const localVars = new Set();
|
|
242
|
+
function visit(node, isRoot) {
|
|
243
|
+
if (!isRoot &&
|
|
244
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
245
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
246
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
250
|
+
for (const declarator of node.declarations) {
|
|
251
|
+
collectBindingNames(declarator.id, localVars);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const key in node) {
|
|
255
|
+
if (key === 'parent' ||
|
|
256
|
+
key === 'range' ||
|
|
257
|
+
key === 'loc' ||
|
|
258
|
+
key === 'type')
|
|
259
|
+
continue;
|
|
260
|
+
const child = node[key];
|
|
261
|
+
if (child && typeof child === 'object') {
|
|
262
|
+
if (Array.isArray(child)) {
|
|
263
|
+
for (const item of child) {
|
|
264
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
265
|
+
visit(item, false);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if ('type' in child) {
|
|
270
|
+
visit(child, false);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
visit(body, true);
|
|
276
|
+
return localVars;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Detects cross-iteration state patterns that require sequential
|
|
280
|
+
* execution:
|
|
281
|
+
*
|
|
282
|
+
* 1. Accumulator: a variable declared OUTSIDE the loop body (i.e., not
|
|
283
|
+
* in localVars) is ASSIGNED inside the loop body. Examples: `total +=
|
|
284
|
+
* value`, `cursor = page.nextCursor`, `previousResult = result`. This
|
|
285
|
+
* catches running totals, pagination cursors, and chained results.
|
|
286
|
+
*
|
|
287
|
+
* 2. Direct cross-await dependency: a variable declared by an await
|
|
288
|
+
* inside the loop is then read as an argument to another await in the
|
|
289
|
+
* same loop body. Example: `const a = await f(); const b = await g(a);`.
|
|
290
|
+
*/
|
|
291
|
+
function hasSequentialDependency(body, loopLocalVars) {
|
|
292
|
+
// Pattern 1: outer variable is written inside the loop body.
|
|
293
|
+
// Collect all assignment targets (left-hand sides of assignments and
|
|
294
|
+
// compound assignments) in the loop body.
|
|
295
|
+
let foundOuterWrite = false;
|
|
296
|
+
function findOuterWrites(node, isRoot) {
|
|
297
|
+
if (foundOuterWrite)
|
|
298
|
+
return;
|
|
299
|
+
if (!isRoot &&
|
|
300
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
301
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
302
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
306
|
+
// Collect identifiers on the left-hand side
|
|
307
|
+
const lhsIds = new Set();
|
|
308
|
+
collectIdentifiers(node.left, lhsIds, false);
|
|
309
|
+
for (const id of lhsIds) {
|
|
310
|
+
if (!loopLocalVars.has(id)) {
|
|
311
|
+
foundOuterWrite = true;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const key in node) {
|
|
317
|
+
if (key === 'parent' ||
|
|
318
|
+
key === 'range' ||
|
|
319
|
+
key === 'loc' ||
|
|
320
|
+
key === 'type')
|
|
321
|
+
continue;
|
|
322
|
+
const child = node[key];
|
|
323
|
+
if (child && typeof child === 'object') {
|
|
324
|
+
if (Array.isArray(child)) {
|
|
325
|
+
for (const item of child) {
|
|
326
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
327
|
+
findOuterWrites(item, false);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if ('type' in child) {
|
|
332
|
+
findOuterWrites(child, false);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
findOuterWrites(body, true);
|
|
338
|
+
if (foundOuterWrite)
|
|
339
|
+
return true;
|
|
340
|
+
// Pattern 2: a variable declared by an await is used as arg to another await.
|
|
341
|
+
const awaitDeclaredVars = new Set();
|
|
342
|
+
function collectAwaitDeclVars(node, isRoot) {
|
|
343
|
+
if (!isRoot &&
|
|
344
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
345
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
346
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
350
|
+
for (const declarator of node.declarations) {
|
|
351
|
+
if (declarator.init &&
|
|
352
|
+
declarator.init.type === utils_1.AST_NODE_TYPES.AwaitExpression) {
|
|
353
|
+
collectBindingNames(declarator.id, awaitDeclaredVars);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const key in node) {
|
|
358
|
+
if (key === 'parent' ||
|
|
359
|
+
key === 'range' ||
|
|
360
|
+
key === 'loc' ||
|
|
361
|
+
key === 'type')
|
|
362
|
+
continue;
|
|
363
|
+
const child = node[key];
|
|
364
|
+
if (child && typeof child === 'object') {
|
|
365
|
+
if (Array.isArray(child)) {
|
|
366
|
+
for (const item of child) {
|
|
367
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
368
|
+
collectAwaitDeclVars(item, false);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if ('type' in child) {
|
|
373
|
+
collectAwaitDeclVars(child, false);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
collectAwaitDeclVars(body, true);
|
|
379
|
+
if (awaitDeclaredVars.size === 0)
|
|
380
|
+
return false;
|
|
381
|
+
let foundCrossAwaitDep = false;
|
|
382
|
+
function findCrossAwaitDep(node, isRoot) {
|
|
383
|
+
if (foundCrossAwaitDep)
|
|
384
|
+
return;
|
|
385
|
+
if (!isRoot &&
|
|
386
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
387
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
388
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (node.type === utils_1.AST_NODE_TYPES.AwaitExpression) {
|
|
392
|
+
const argIds = new Set();
|
|
393
|
+
collectIdentifiers(node.argument, argIds, false);
|
|
394
|
+
for (const varName of awaitDeclaredVars) {
|
|
395
|
+
if (argIds.has(varName)) {
|
|
396
|
+
foundCrossAwaitDep = true;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const key in node) {
|
|
402
|
+
if (key === 'parent' ||
|
|
403
|
+
key === 'range' ||
|
|
404
|
+
key === 'loc' ||
|
|
405
|
+
key === 'type')
|
|
406
|
+
continue;
|
|
407
|
+
const child = node[key];
|
|
408
|
+
if (child && typeof child === 'object') {
|
|
409
|
+
if (Array.isArray(child)) {
|
|
410
|
+
for (const item of child) {
|
|
411
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
412
|
+
findCrossAwaitDep(item, false);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if ('type' in child) {
|
|
417
|
+
findCrossAwaitDep(child, false);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
findCrossAwaitDep(body, true);
|
|
423
|
+
return foundCrossAwaitDep;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Extracts all binding names from a pattern (handles identifiers,
|
|
427
|
+
* object/array destructuring, rest elements, etc.).
|
|
428
|
+
*/
|
|
429
|
+
function collectBindingNames(pattern, names) {
|
|
430
|
+
switch (pattern.type) {
|
|
431
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
432
|
+
names.add(pattern.name);
|
|
433
|
+
break;
|
|
434
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
435
|
+
for (const prop of pattern.properties) {
|
|
436
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
437
|
+
collectBindingNames(prop.value, names);
|
|
438
|
+
}
|
|
439
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
440
|
+
collectBindingNames(prop.argument, names);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
445
|
+
for (const el of pattern.elements) {
|
|
446
|
+
if (el)
|
|
447
|
+
collectBindingNames(el, names);
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
451
|
+
collectBindingNames(pattern.argument, names);
|
|
452
|
+
break;
|
|
453
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
454
|
+
collectBindingNames(pattern.left, names);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Walks the loop body looking for the first AwaitExpression that is NOT
|
|
460
|
+
* inside a nested async function (a different async scope). Returns the
|
|
461
|
+
* first such AwaitExpression found, or null if none exists.
|
|
462
|
+
*/
|
|
463
|
+
function findDirectAwait(node, isRoot) {
|
|
464
|
+
// Do not cross into nested functions; their awaits belong to a
|
|
465
|
+
// different async scope
|
|
466
|
+
if (!isRoot &&
|
|
467
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
468
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
469
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (node.type === utils_1.AST_NODE_TYPES.AwaitExpression) {
|
|
473
|
+
return node;
|
|
474
|
+
}
|
|
475
|
+
for (const key in node) {
|
|
476
|
+
if (key === 'parent' ||
|
|
477
|
+
key === 'range' ||
|
|
478
|
+
key === 'loc' ||
|
|
479
|
+
key === 'type')
|
|
480
|
+
continue;
|
|
481
|
+
const child = node[key];
|
|
482
|
+
if (child && typeof child === 'object') {
|
|
483
|
+
if (Array.isArray(child)) {
|
|
484
|
+
for (const item of child) {
|
|
485
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
486
|
+
const result = findDirectAwait(item, false);
|
|
487
|
+
if (result)
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else if ('type' in child) {
|
|
493
|
+
const result = findDirectAwait(child, false);
|
|
494
|
+
if (result)
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Central analysis for a loop node. Returns the AwaitExpression to
|
|
503
|
+
* report on, or null if the loop should not be flagged.
|
|
504
|
+
*/
|
|
505
|
+
function analyzeLoop(loopNode) {
|
|
506
|
+
const body = loopNode.body;
|
|
507
|
+
if (!body)
|
|
508
|
+
return null;
|
|
509
|
+
// Find an await directly inside the loop body (not in nested async fns)
|
|
510
|
+
const awaitExpr = findDirectAwait(body, true);
|
|
511
|
+
if (!awaitExpr)
|
|
512
|
+
return null;
|
|
513
|
+
// Exclusion: loop body contains a coordinator (batch, transaction, etc.)
|
|
514
|
+
if (hasCoordinatorInBody(body))
|
|
515
|
+
return null;
|
|
516
|
+
// Exclusion: loop body contains a rate-limiting call (sleep, delay, etc.)
|
|
517
|
+
if (hasRateLimitedCallInBody(body))
|
|
518
|
+
return null;
|
|
519
|
+
// Exclusion: the await is inside a try/catch — per-iteration error
|
|
520
|
+
// handling implies intentional sequential execution
|
|
521
|
+
if (isAwaitInTryCatch(awaitExpr, loopNode))
|
|
522
|
+
return null;
|
|
523
|
+
// Exclusion: loop body contains break/continue/return — control flow
|
|
524
|
+
// depends on async result
|
|
525
|
+
if (containsBreakContinueReturn(body))
|
|
526
|
+
return null;
|
|
527
|
+
// Exclusion: accumulator / pagination patterns — sequential dependency
|
|
528
|
+
// detected between iterations
|
|
529
|
+
const loopLocalVars = collectLoopLocalVars(body);
|
|
530
|
+
if (hasSequentialDependency(body, loopLocalVars))
|
|
531
|
+
return null;
|
|
532
|
+
// Exclusion: the specific await being reported is a rate-limiting call
|
|
533
|
+
const callNames = getCallNames(awaitExpr);
|
|
534
|
+
for (const name of callNames) {
|
|
535
|
+
for (const pattern of rateLimitedPatterns) {
|
|
536
|
+
if (name.toLowerCase() === pattern.toLowerCase())
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return awaitExpr;
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
ForOfStatement(node) {
|
|
544
|
+
const awaitExpr = analyzeLoop(node);
|
|
545
|
+
if (awaitExpr) {
|
|
546
|
+
context.report({
|
|
547
|
+
node: awaitExpr,
|
|
548
|
+
messageId: 'parallelizeLoopAwaits',
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
ForInStatement(node) {
|
|
553
|
+
const awaitExpr = analyzeLoop(node);
|
|
554
|
+
if (awaitExpr) {
|
|
555
|
+
context.report({
|
|
556
|
+
node: awaitExpr,
|
|
557
|
+
messageId: 'parallelizeLoopAwaits',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
ForStatement(node) {
|
|
562
|
+
const awaitExpr = analyzeLoop(node);
|
|
563
|
+
if (awaitExpr) {
|
|
564
|
+
context.report({
|
|
565
|
+
node: awaitExpr,
|
|
566
|
+
messageId: 'parallelizeLoopAwaits',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
WhileStatement(node) {
|
|
571
|
+
const awaitExpr = analyzeLoop(node);
|
|
572
|
+
if (awaitExpr) {
|
|
573
|
+
context.report({
|
|
574
|
+
node: awaitExpr,
|
|
575
|
+
messageId: 'parallelizeLoopAwaits',
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
//# sourceMappingURL=parallelize-loop-awaits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const preferFlatTransformEachKeys: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferFlatTransformEachKeys", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|