@blumintinc/eslint-plugin-blumint 1.20.106 → 1.20.108
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/lib/index.js
CHANGED
|
@@ -474,6 +474,142 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
474
474
|
}
|
|
475
475
|
return names;
|
|
476
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Records the binding a single assignment target writes to.
|
|
479
|
+
*
|
|
480
|
+
* A member write records its ROOT object (`obj.a.b = 1` yields `obj`),
|
|
481
|
+
* because the state it mutates is reachable through that binding, and a
|
|
482
|
+
* later await naming the object observes the mutation. Optional chains and
|
|
483
|
+
* TS wrappers (`obj!.x = 1`) are unwrapped so the root is still found.
|
|
484
|
+
* Destructuring targets recurse to their leaf identifiers, since
|
|
485
|
+
* `({ a } = source)` and `[a] = source` write `a` just as `a = source.a`
|
|
486
|
+
* does.
|
|
487
|
+
*/
|
|
488
|
+
function collectAssignmentTarget(target, targets) {
|
|
489
|
+
switch (target.type) {
|
|
490
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
491
|
+
targets.push(target);
|
|
492
|
+
break;
|
|
493
|
+
case utils_1.AST_NODE_TYPES.MemberExpression: {
|
|
494
|
+
let root = target;
|
|
495
|
+
for (;;) {
|
|
496
|
+
if (root.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
497
|
+
root = root.object;
|
|
498
|
+
}
|
|
499
|
+
else if (root.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
500
|
+
root.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
501
|
+
root.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
502
|
+
root = root.expression;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (root.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
509
|
+
targets.push(root);
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
514
|
+
for (const property of target.properties) {
|
|
515
|
+
if (property.type === utils_1.AST_NODE_TYPES.Property) {
|
|
516
|
+
collectAssignmentTarget(property.value, targets);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
collectAssignmentTarget(property.argument, targets);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
524
|
+
for (const element of target.elements) {
|
|
525
|
+
if (element) {
|
|
526
|
+
collectAssignmentTarget(element, targets);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
531
|
+
collectAssignmentTarget(target.argument, targets);
|
|
532
|
+
break;
|
|
533
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
534
|
+
collectAssignmentTarget(target.left, targets);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Reports whether an assignment target resolves to a binding DECLARED
|
|
540
|
+
* inside the given expression.
|
|
541
|
+
*
|
|
542
|
+
* Such a binding is a fresh local: `async () => { let tmp; tmp = 1; }`
|
|
543
|
+
* publishes nothing to the enclosing scope, so a later await mentioning
|
|
544
|
+
* `tmp` is reading some other binding entirely. An unresolved name (an
|
|
545
|
+
* implicit global) counts as external, which keeps the barrier in place for
|
|
546
|
+
* the case the analysis cannot see.
|
|
547
|
+
*/
|
|
548
|
+
function isDeclaredWithin(identifier, root) {
|
|
549
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
|
|
550
|
+
if (!variable || variable.defs.length === 0) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
return variable.defs.every((definition) => definition.name.range[0] >= root.range[0] &&
|
|
554
|
+
definition.name.range[1] <= root.range[1]);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Collects the identifier names an awaited expression WRITES.
|
|
558
|
+
*
|
|
559
|
+
* The traversal deliberately crosses function boundaries, which is the
|
|
560
|
+
* opposite of what containsSuspendingAwait needs: the write that matters
|
|
561
|
+
* lives inside the callback handed to the awaited call. `await
|
|
562
|
+
* db.runTransaction(async (tx) => { mutator = new Mutator(tx); })` publishes
|
|
563
|
+
* `mutator` to the enclosing scope by the time it settles, so the callback
|
|
564
|
+
* body is part of what that statement does, not a separate deferred unit.
|
|
565
|
+
*
|
|
566
|
+
* A `VariableDeclarator` id is deliberately NOT a write: `const x = ...`
|
|
567
|
+
* inside a callback creates a fresh local binding rather than publishing a
|
|
568
|
+
* value to an outer one, so it cannot be what a later await reads.
|
|
569
|
+
*/
|
|
570
|
+
function getAssignedNames(node) {
|
|
571
|
+
const targets = [];
|
|
572
|
+
const visit = (current) => {
|
|
573
|
+
if (current.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
574
|
+
collectAssignmentTarget(current.left, targets);
|
|
575
|
+
}
|
|
576
|
+
else if (current.type === utils_1.AST_NODE_TYPES.UpdateExpression) {
|
|
577
|
+
collectAssignmentTarget(current.argument, targets);
|
|
578
|
+
}
|
|
579
|
+
else if ((current.type === utils_1.AST_NODE_TYPES.ForOfStatement ||
|
|
580
|
+
current.type === utils_1.AST_NODE_TYPES.ForInStatement) &&
|
|
581
|
+
current.left.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
582
|
+
// `for (captured of items)` assigns an existing binding on every
|
|
583
|
+
// iteration; only the declaration form introduces a fresh local.
|
|
584
|
+
collectAssignmentTarget(current.left, targets);
|
|
585
|
+
}
|
|
586
|
+
for (const key in current) {
|
|
587
|
+
if (key === 'parent' || key === 'range' || key === 'loc')
|
|
588
|
+
continue;
|
|
589
|
+
const child = current[key];
|
|
590
|
+
if (!child || typeof child !== 'object')
|
|
591
|
+
continue;
|
|
592
|
+
if (Array.isArray(child)) {
|
|
593
|
+
for (const item of child) {
|
|
594
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
595
|
+
visit(item);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else if ('type' in child) {
|
|
600
|
+
visit(child);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
visit(node);
|
|
605
|
+
const names = new Set();
|
|
606
|
+
for (const target of targets) {
|
|
607
|
+
if (!isDeclaredWithin(target, node)) {
|
|
608
|
+
names.add(target.name);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return names;
|
|
612
|
+
}
|
|
477
613
|
/**
|
|
478
614
|
* Checks if there are dependencies between await expressions
|
|
479
615
|
*/
|
|
@@ -671,6 +807,37 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
671
807
|
return true;
|
|
672
808
|
}
|
|
673
809
|
}
|
|
810
|
+
// 10. Closure-write barrier: read after write. An awaited call whose
|
|
811
|
+
// callback ASSIGNS an outer binding carries a data dependency that no
|
|
812
|
+
// value flowing out of the await expresses, so `variableNames` -- which
|
|
813
|
+
// holds only the names the run's own statements DECLARE -- cannot see it.
|
|
814
|
+
// `let mutator; await db.runTransaction(async (tx) => { mutator = new
|
|
815
|
+
// Mutator(tx); }); await mutator?.deleteIfEmptied();` is the shape. The
|
|
816
|
+
// rewrite is silently wrong rather than merely eager: array elements
|
|
817
|
+
// evaluate left to right at construction time, so the second element runs
|
|
818
|
+
// while `mutator` is still undefined -- the optional chain short-circuits
|
|
819
|
+
// and the operation NEVER happens, with no error to reveal it. Dropping
|
|
820
|
+
// the `?.` turns the same rewrite into a TypeError instead. Keyed on a
|
|
821
|
+
// write that a LATER await actually reads, so a callback whose effects
|
|
822
|
+
// nothing downstream observes still parallelizes. (#1723)
|
|
823
|
+
const assignedNames = awaitNodes.map((node) => {
|
|
824
|
+
const awaitExpr = getAwaitExpression(node);
|
|
825
|
+
return awaitExpr
|
|
826
|
+
? getAssignedNames(awaitExpr.argument)
|
|
827
|
+
: new Set();
|
|
828
|
+
});
|
|
829
|
+
for (let i = 1; i < awaitNodes.length; i++) {
|
|
830
|
+
const currentIds = allIdentifiers[i];
|
|
831
|
+
if (currentIds.size === 0)
|
|
832
|
+
continue;
|
|
833
|
+
for (let j = 0; j < i; j++) {
|
|
834
|
+
for (const written of assignedNames[j]) {
|
|
835
|
+
if (currentIds.has(written)) {
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
674
841
|
return false;
|
|
675
842
|
}
|
|
676
843
|
/**
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parallelizeLoopAwaits = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
// Anchored at the end of the path so multi-part suffixes such as
|
|
7
8
|
// `EventRegistry.integration.test.ts` are recognized while production modules
|
|
8
9
|
// that merely contain the word (`testHelpers.ts`, `latest.ts`, `contest/Thing.ts`)
|
|
@@ -291,85 +292,162 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
291
292
|
return false;
|
|
292
293
|
}
|
|
293
294
|
/**
|
|
294
|
-
* Collects
|
|
295
|
-
*
|
|
296
|
-
*
|
|
295
|
+
* Collects the IDENTIFIERS an assignment target writes through, returning
|
|
296
|
+
* false when the target's root is not a plain binding at all.
|
|
297
|
+
*
|
|
298
|
+
* A member write reaches the object its ROOT names: `box.value = 1` writes
|
|
299
|
+
* through `box`, and `value` is a field label that binds nothing — the same
|
|
300
|
+
* distinction drawn for a non-computed property key (#1688). Counting the
|
|
301
|
+
* label as a written name makes every member write look like a write to an
|
|
302
|
+
* outer binding, which is what hides a callback writing through its own
|
|
303
|
+
* parameter (`async (page) => { page.total = 1 }`).
|
|
304
|
+
*
|
|
305
|
+
* A root the analysis cannot name — `this.count += 1` reaches instance
|
|
306
|
+
* state every iteration shares — returns false, and the caller reads that
|
|
307
|
+
* as an outer write. The plugin prefers a missed report to a spurious one.
|
|
308
|
+
*
|
|
309
|
+
* The identifier NODE is carried rather than its name, because locality is
|
|
310
|
+
* a question about scope: two bindings can share a spelling, and only the
|
|
311
|
+
* node knows which one a given write reaches. (#1725)
|
|
297
312
|
*/
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
313
|
+
function collectAssignmentTargetIdentifiers(target, identifiers) {
|
|
314
|
+
switch (target.type) {
|
|
315
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
316
|
+
identifiers.push(target);
|
|
317
|
+
return true;
|
|
318
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
319
|
+
return collectAssignmentTargetIdentifiers(target.object, identifiers);
|
|
320
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
321
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
322
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
323
|
+
return collectAssignmentTargetIdentifiers(target.expression, identifiers);
|
|
324
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern: {
|
|
325
|
+
let resolved = true;
|
|
326
|
+
for (const property of target.properties) {
|
|
327
|
+
const inner = property.type === utils_1.AST_NODE_TYPES.RestElement
|
|
328
|
+
? property.argument
|
|
329
|
+
: property.value;
|
|
330
|
+
if (!collectAssignmentTargetIdentifiers(inner, identifiers)) {
|
|
331
|
+
resolved = false;
|
|
332
|
+
}
|
|
310
333
|
}
|
|
334
|
+
return resolved;
|
|
311
335
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const child = node[key];
|
|
319
|
-
if (child && typeof child === 'object') {
|
|
320
|
-
if (Array.isArray(child)) {
|
|
321
|
-
for (const item of child) {
|
|
322
|
-
if (item && typeof item === 'object' && 'type' in item) {
|
|
323
|
-
visit(item, false);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
else if ('type' in child) {
|
|
328
|
-
visit(child, false);
|
|
336
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern: {
|
|
337
|
+
let resolved = true;
|
|
338
|
+
for (const element of target.elements) {
|
|
339
|
+
if (element &&
|
|
340
|
+
!collectAssignmentTargetIdentifiers(element, identifiers)) {
|
|
341
|
+
resolved = false;
|
|
329
342
|
}
|
|
330
343
|
}
|
|
344
|
+
return resolved;
|
|
331
345
|
}
|
|
346
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
347
|
+
return collectAssignmentTargetIdentifiers(target.argument, identifiers);
|
|
348
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
349
|
+
return collectAssignmentTargetIdentifiers(target.left, identifiers);
|
|
350
|
+
default:
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Reports whether an identifier resolves to a binding DECLARED inside the
|
|
356
|
+
* given root node.
|
|
357
|
+
*
|
|
358
|
+
* Such a binding is iteration-local: nothing it holds outlives the
|
|
359
|
+
* iteration that created it, so writing it couples no two iterations.
|
|
360
|
+
* `async () => { let tmp; tmp = 1; }` publishes nothing to the enclosing
|
|
361
|
+
* scope, and a callback's parameters bind afresh on every invocation.
|
|
362
|
+
*
|
|
363
|
+
* The question is settled by SCOPE rather than by spelling. A flat set of
|
|
364
|
+
* declared NAMES cannot tell an outer binding from a nested one that merely
|
|
365
|
+
* reuses the identifier, so a genuine cross-iteration write to `cursor`
|
|
366
|
+
* would read as local the moment any callback in the loop happened to name
|
|
367
|
+
* a parameter `cursor`. (#1725)
|
|
368
|
+
*
|
|
369
|
+
* An UNRESOLVED name — an implicit global — counts as external, which keeps
|
|
370
|
+
* the barrier in place for the case the analysis cannot see.
|
|
371
|
+
*/
|
|
372
|
+
function isDeclaredWithin(identifier, root) {
|
|
373
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
|
|
374
|
+
if (!variable || variable.defs.length === 0) {
|
|
375
|
+
return false;
|
|
332
376
|
}
|
|
333
|
-
|
|
334
|
-
|
|
377
|
+
return variable.defs.every((definition) => definition.name.range[0] >= root.range[0] &&
|
|
378
|
+
definition.name.range[1] <= root.range[1]);
|
|
335
379
|
}
|
|
336
380
|
/**
|
|
337
381
|
* Detects cross-iteration state patterns that require sequential
|
|
338
382
|
* execution:
|
|
339
383
|
*
|
|
340
|
-
* 1. Accumulator: a variable declared OUTSIDE the loop body
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
384
|
+
* 1. Accumulator: a variable declared OUTSIDE the loop body is ASSIGNED
|
|
385
|
+
* inside it, whether directly or from inside a callback the body hands
|
|
386
|
+
* to the awaited call. Examples: `total += value`, `cursor =
|
|
387
|
+
* page.nextCursor`, `previousResult = result`. This catches running
|
|
388
|
+
* totals, pagination cursors, and chained results.
|
|
344
389
|
*
|
|
345
390
|
* 2. Direct cross-await dependency: a variable declared by an await
|
|
346
391
|
* inside the loop is then read as an argument to another await in the
|
|
347
392
|
* same loop body. Example: `const a = await f(); const b = await g(a);`.
|
|
348
393
|
*/
|
|
349
|
-
function hasSequentialDependency(body
|
|
394
|
+
function hasSequentialDependency(body) {
|
|
350
395
|
// Pattern 1: outer variable is written inside the loop body.
|
|
351
|
-
// Collect
|
|
352
|
-
// compound
|
|
396
|
+
// Collect every assignment target — the left-hand side of an assignment
|
|
397
|
+
// or compound assignment, and the operand of an increment in a callback.
|
|
353
398
|
let foundOuterWrite = false;
|
|
354
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Reports whether an assignment target reaches a binding the iterations
|
|
401
|
+
* share rather than one the iteration creates.
|
|
402
|
+
*
|
|
403
|
+
* The body is the locality root, so a binding introduced by the loop's
|
|
404
|
+
* own HEAD reads as shared. That is the conservative reading and the
|
|
405
|
+
* correct one for a C-style counter: `for (let i = 0; i < n; i += 1)`
|
|
406
|
+
* carries `i` forward between iterations, so a body write to it really
|
|
407
|
+
* does couple them.
|
|
408
|
+
*/
|
|
409
|
+
function writesOuterBinding(target) {
|
|
410
|
+
const identifiers = [];
|
|
411
|
+
if (!collectAssignmentTargetIdentifiers(target, identifiers)) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
for (const identifier of identifiers) {
|
|
415
|
+
if (!isDeclaredWithin(identifier, body))
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
function findOuterWrites(node, isRoot, inNestedFunction) {
|
|
355
421
|
if (foundOuterWrite)
|
|
356
422
|
return;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
423
|
+
// The walk deliberately enters callbacks. A write handed to the awaited
|
|
424
|
+
// call is the same data dependency as one written beside it:
|
|
425
|
+
// `await run(item, async (page) => { cursor = page.nextCursor })` has
|
|
426
|
+
// settled — and published `cursor` — by the time the iteration ends, so
|
|
427
|
+
// parallel iterations would race exactly as they would over
|
|
428
|
+
// `cursor = await run(item, cursor)`. (#1724)
|
|
429
|
+
const isNested = inNestedFunction ||
|
|
430
|
+
(!isRoot &&
|
|
431
|
+
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
432
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
433
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression));
|
|
434
|
+
if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
435
|
+
writesOuterBinding(node.left)) {
|
|
436
|
+
foundOuterWrite = true;
|
|
361
437
|
return;
|
|
362
438
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
439
|
+
// An increment counts only inside a callback. At the loop-body level it
|
|
440
|
+
// is the loop's own step counter — `while (i < n) { await f(items[i]);
|
|
441
|
+
// i++; }` walks the iteration space, and the `Promise.all(items.map(
|
|
442
|
+
// ...))` rewrite subsumes it — whereas a callback steps no iteration:
|
|
443
|
+
// `count++` there folds what the awaited work produced into a binding
|
|
444
|
+
// the whole loop shares, carrying the same dependency as the compound
|
|
445
|
+
// assignment it stands in for. (#1724)
|
|
446
|
+
if (isNested &&
|
|
447
|
+
node.type === utils_1.AST_NODE_TYPES.UpdateExpression &&
|
|
448
|
+
writesOuterBinding(node.argument)) {
|
|
449
|
+
foundOuterWrite = true;
|
|
450
|
+
return;
|
|
373
451
|
}
|
|
374
452
|
for (const key in node) {
|
|
375
453
|
if (key === 'parent' ||
|
|
@@ -382,17 +460,17 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
382
460
|
if (Array.isArray(child)) {
|
|
383
461
|
for (const item of child) {
|
|
384
462
|
if (item && typeof item === 'object' && 'type' in item) {
|
|
385
|
-
findOuterWrites(item, false);
|
|
463
|
+
findOuterWrites(item, false, isNested);
|
|
386
464
|
}
|
|
387
465
|
}
|
|
388
466
|
}
|
|
389
467
|
else if ('type' in child) {
|
|
390
|
-
findOuterWrites(child, false);
|
|
468
|
+
findOuterWrites(child, false, isNested);
|
|
391
469
|
}
|
|
392
470
|
}
|
|
393
471
|
}
|
|
394
472
|
}
|
|
395
|
-
findOuterWrites(body, true);
|
|
473
|
+
findOuterWrites(body, true, false);
|
|
396
474
|
if (foundOuterWrite)
|
|
397
475
|
return true;
|
|
398
476
|
// Pattern 2: a variable declared by an await is used as arg to another await.
|
|
@@ -736,8 +814,7 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
736
814
|
return null;
|
|
737
815
|
// Exclusion: accumulator / pagination patterns — sequential dependency
|
|
738
816
|
// detected between iterations
|
|
739
|
-
|
|
740
|
-
if (hasSequentialDependency(body, loopLocalVars))
|
|
817
|
+
if (hasSequentialDependency(body))
|
|
741
818
|
return null;
|
|
742
819
|
// Exclusion: the specific await being reported is a rate-limiting call
|
|
743
820
|
const callNames = getCallNames(awaitExpr);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.108",
|
|
4
|
+
"date": "2026-08-05T05:55:32.346Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "parallelize-loop-awaits",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1725
|
|
11
|
+
],
|
|
12
|
+
"summary": "resolve write locality by scope, not by name (closes #1725)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.107",
|
|
18
|
+
"date": "2026-08-05T05:11:58.347Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "parallelize-async-operations",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1723
|
|
25
|
+
],
|
|
26
|
+
"summary": "treat a callback's write to an outer binding as a dependency (closes #1723)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "parallelize-loop-awaits",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1724
|
|
33
|
+
],
|
|
34
|
+
"summary": "see a callback's write to an outer binding (closes #1724)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.106",
|
|
4
40
|
"date": "2026-08-05T01:12:16.076Z",
|