@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.
Files changed (51) hide show
  1. package/README.md +21 -0
  2. package/lib/index.js +64 -1
  3. package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
  4. package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
  5. package/lib/rules/enforce-cloud-function-id-length.js +104 -0
  6. package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
  7. package/lib/rules/enforce-is-prefix-validators.js +304 -0
  8. package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
  9. package/lib/rules/enforce-m3-sentence-case.js +430 -0
  10. package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
  11. package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
  12. package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
  13. package/lib/rules/enforce-types-directory-placement.js +276 -0
  14. package/lib/rules/no-direct-function-state.d.ts +8 -0
  15. package/lib/rules/no-direct-function-state.js +285 -0
  16. package/lib/rules/no-fill-template-mutation.d.ts +1 -0
  17. package/lib/rules/no-fill-template-mutation.js +324 -0
  18. package/lib/rules/no-hungarian.js +18 -26
  19. package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
  20. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  21. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  22. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  23. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  24. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  25. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  26. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  27. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  28. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  29. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  30. package/lib/rules/parallelize-loop-awaits.js +582 -0
  31. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  32. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  33. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  34. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  35. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  36. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  37. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  38. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  39. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  40. package/lib/rules/prefer-use-base62-id.js +483 -0
  41. package/lib/rules/prefer-use-theme.d.ts +1 -0
  42. package/lib/rules/prefer-use-theme.js +206 -0
  43. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  44. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  45. package/lib/rules/react-memoize-literals.js +106 -1
  46. package/lib/rules/require-props-composition.d.ts +10 -0
  47. package/lib/rules/require-props-composition.js +433 -0
  48. package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
  49. package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
  50. package/package.json +1 -1
  51. 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>;