@blumintinc/eslint-plugin-blumint 1.20.107 → 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 +1 -1
- package/lib/rules/parallelize-loop-awaits.js +60 -76
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -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,61 +292,7 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
291
292
|
return false;
|
|
292
293
|
}
|
|
293
294
|
/**
|
|
294
|
-
* Collects
|
|
295
|
-
* callbacks written there. These are iteration-local variables: nothing
|
|
296
|
-
* they hold outlives the iteration that created them.
|
|
297
|
-
*
|
|
298
|
-
* The walk crosses nested function boundaries because the write scan that
|
|
299
|
-
* consults this set crosses them too. A name both declared and assigned
|
|
300
|
-
* inside a callback (`async () => { let tmp; tmp = 1; }`) publishes nothing
|
|
301
|
-
* to the enclosing scope, so if the set stopped at the boundary the write
|
|
302
|
-
* would read as a cross-iteration dependency and silence the loop. (#1724)
|
|
303
|
-
*/
|
|
304
|
-
function collectLoopLocalVars(body) {
|
|
305
|
-
const localVars = new Set();
|
|
306
|
-
function visit(node, isRoot) {
|
|
307
|
-
// A callback's parameters bind afresh on every invocation, so a write
|
|
308
|
-
// through one (`async (page) => { page.total = 1 }`) reaches whatever
|
|
309
|
-
// the caller handed that call rather than state the iterations share.
|
|
310
|
-
if (!isRoot &&
|
|
311
|
-
(node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
312
|
-
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
313
|
-
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
314
|
-
for (const param of node.params) {
|
|
315
|
-
collectBindingNames(param, localVars);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
319
|
-
for (const declarator of node.declarations) {
|
|
320
|
-
collectBindingNames(declarator.id, localVars);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
for (const key in node) {
|
|
324
|
-
if (key === 'parent' ||
|
|
325
|
-
key === 'range' ||
|
|
326
|
-
key === 'loc' ||
|
|
327
|
-
key === 'type')
|
|
328
|
-
continue;
|
|
329
|
-
const child = node[key];
|
|
330
|
-
if (child && typeof child === 'object') {
|
|
331
|
-
if (Array.isArray(child)) {
|
|
332
|
-
for (const item of child) {
|
|
333
|
-
if (item && typeof item === 'object' && 'type' in item) {
|
|
334
|
-
visit(item, false);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
else if ('type' in child) {
|
|
339
|
-
visit(child, false);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
visit(body, true);
|
|
345
|
-
return localVars;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Collects the BINDINGS an assignment target writes through, returning
|
|
295
|
+
* Collects the IDENTIFIERS an assignment target writes through, returning
|
|
349
296
|
* false when the target's root is not a plain binding at all.
|
|
350
297
|
*
|
|
351
298
|
* A member write reaches the object its ROOT names: `box.value = 1` writes
|
|
@@ -358,62 +305,93 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
358
305
|
* A root the analysis cannot name — `this.count += 1` reaches instance
|
|
359
306
|
* state every iteration shares — returns false, and the caller reads that
|
|
360
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)
|
|
361
312
|
*/
|
|
362
|
-
function
|
|
313
|
+
function collectAssignmentTargetIdentifiers(target, identifiers) {
|
|
363
314
|
switch (target.type) {
|
|
364
315
|
case utils_1.AST_NODE_TYPES.Identifier:
|
|
365
|
-
|
|
316
|
+
identifiers.push(target);
|
|
366
317
|
return true;
|
|
367
318
|
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
368
|
-
return
|
|
319
|
+
return collectAssignmentTargetIdentifiers(target.object, identifiers);
|
|
369
320
|
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
370
321
|
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
371
322
|
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
372
|
-
return
|
|
323
|
+
return collectAssignmentTargetIdentifiers(target.expression, identifiers);
|
|
373
324
|
case utils_1.AST_NODE_TYPES.ObjectPattern: {
|
|
374
325
|
let resolved = true;
|
|
375
326
|
for (const property of target.properties) {
|
|
376
327
|
const inner = property.type === utils_1.AST_NODE_TYPES.RestElement
|
|
377
328
|
? property.argument
|
|
378
329
|
: property.value;
|
|
379
|
-
if (!
|
|
330
|
+
if (!collectAssignmentTargetIdentifiers(inner, identifiers)) {
|
|
380
331
|
resolved = false;
|
|
332
|
+
}
|
|
381
333
|
}
|
|
382
334
|
return resolved;
|
|
383
335
|
}
|
|
384
336
|
case utils_1.AST_NODE_TYPES.ArrayPattern: {
|
|
385
337
|
let resolved = true;
|
|
386
338
|
for (const element of target.elements) {
|
|
387
|
-
if (element &&
|
|
339
|
+
if (element &&
|
|
340
|
+
!collectAssignmentTargetIdentifiers(element, identifiers)) {
|
|
388
341
|
resolved = false;
|
|
389
342
|
}
|
|
390
343
|
}
|
|
391
344
|
return resolved;
|
|
392
345
|
}
|
|
393
346
|
case utils_1.AST_NODE_TYPES.RestElement:
|
|
394
|
-
return
|
|
347
|
+
return collectAssignmentTargetIdentifiers(target.argument, identifiers);
|
|
395
348
|
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
396
|
-
return
|
|
349
|
+
return collectAssignmentTargetIdentifiers(target.left, identifiers);
|
|
397
350
|
default:
|
|
398
351
|
return false;
|
|
399
352
|
}
|
|
400
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;
|
|
376
|
+
}
|
|
377
|
+
return variable.defs.every((definition) => definition.name.range[0] >= root.range[0] &&
|
|
378
|
+
definition.name.range[1] <= root.range[1]);
|
|
379
|
+
}
|
|
401
380
|
/**
|
|
402
381
|
* Detects cross-iteration state patterns that require sequential
|
|
403
382
|
* execution:
|
|
404
383
|
*
|
|
405
|
-
* 1. Accumulator: a variable declared OUTSIDE the loop body
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* results.
|
|
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.
|
|
411
389
|
*
|
|
412
390
|
* 2. Direct cross-await dependency: a variable declared by an await
|
|
413
391
|
* inside the loop is then read as an argument to another await in the
|
|
414
392
|
* same loop body. Example: `const a = await f(); const b = await g(a);`.
|
|
415
393
|
*/
|
|
416
|
-
function hasSequentialDependency(body
|
|
394
|
+
function hasSequentialDependency(body) {
|
|
417
395
|
// Pattern 1: outer variable is written inside the loop body.
|
|
418
396
|
// Collect every assignment target — the left-hand side of an assignment
|
|
419
397
|
// or compound assignment, and the operand of an increment in a callback.
|
|
@@ -421,13 +399,20 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
421
399
|
/**
|
|
422
400
|
* Reports whether an assignment target reaches a binding the iterations
|
|
423
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.
|
|
424
408
|
*/
|
|
425
409
|
function writesOuterBinding(target) {
|
|
426
|
-
const
|
|
427
|
-
if (!
|
|
410
|
+
const identifiers = [];
|
|
411
|
+
if (!collectAssignmentTargetIdentifiers(target, identifiers)) {
|
|
428
412
|
return true;
|
|
429
|
-
|
|
430
|
-
|
|
413
|
+
}
|
|
414
|
+
for (const identifier of identifiers) {
|
|
415
|
+
if (!isDeclaredWithin(identifier, body))
|
|
431
416
|
return true;
|
|
432
417
|
}
|
|
433
418
|
return false;
|
|
@@ -829,8 +814,7 @@ exports.parallelizeLoopAwaits = (0, createRule_1.createRule)({
|
|
|
829
814
|
return null;
|
|
830
815
|
// Exclusion: accumulator / pagination patterns — sequential dependency
|
|
831
816
|
// detected between iterations
|
|
832
|
-
|
|
833
|
-
if (hasSequentialDependency(body, loopLocalVars))
|
|
817
|
+
if (hasSequentialDependency(body))
|
|
834
818
|
return null;
|
|
835
819
|
// Exclusion: the specific await being reported is a rate-limiting call
|
|
836
820
|
const callNames = getCallNames(awaitExpr);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
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
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.107",
|
|
4
18
|
"date": "2026-08-05T05:11:58.347Z",
|