@blumintinc/eslint-plugin-blumint 1.20.52 → 1.20.53

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.52',
226
+ version: '1.20.53',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parallelizeAsyncOperations = 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`)
@@ -334,6 +335,145 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
334
335
  }
335
336
  return callee.object.name;
336
337
  }
338
+ /**
339
+ * Matches the `Promise` combinators that take an array of promises and
340
+ * return a single promise standing in for the whole group. Awaiting one of
341
+ * them does not consume the member promises: they stay reachable through
342
+ * whatever names fed the combinator, so a later await mentioning one of
343
+ * those names is reading a promise the aggregate already owns. Anchored so
344
+ * only the combinator itself matches, never a user helper whose name merely
345
+ * contains the word.
346
+ */
347
+ const PROMISE_AGGREGATOR_PATTERN = /^(all|allSettled|any|race)$/;
348
+ /**
349
+ * Node types that open a new function body. Traversals that ask "does
350
+ * evaluating this expression suspend the enclosing async function?" must
351
+ * stop here, because an `await` beyond this boundary belongs to the inner
352
+ * function and runs only when that function is called.
353
+ */
354
+ const FUNCTION_BOUNDARY_TYPES = new Set([
355
+ utils_1.AST_NODE_TYPES.FunctionDeclaration,
356
+ utils_1.AST_NODE_TYPES.FunctionExpression,
357
+ utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
358
+ ]);
359
+ /**
360
+ * Reports whether evaluating this expression suspends the enclosing async
361
+ * function -- i.e. whether it contains an `await` of its own.
362
+ *
363
+ * The traversal deliberately stops at every function boundary: the awaits in
364
+ * `Promise.all(items.map(async (item) => await store(item)))` belong to the
365
+ * callback, so the outer expression evaluates straight through to a promise
366
+ * without ever suspending, and hoisting it is safe.
367
+ */
368
+ function containsSuspendingAwait(node) {
369
+ if (isAwaitExpression(node)) {
370
+ return true;
371
+ }
372
+ if (FUNCTION_BOUNDARY_TYPES.has(node.type)) {
373
+ return false;
374
+ }
375
+ for (const key in node) {
376
+ if (key === 'parent' || key === 'range' || key === 'loc')
377
+ continue;
378
+ const child = node[key];
379
+ if (!child || typeof child !== 'object')
380
+ continue;
381
+ if (Array.isArray(child)) {
382
+ for (const item of child) {
383
+ if (item &&
384
+ typeof item === 'object' &&
385
+ 'type' in item &&
386
+ containsSuspendingAwait(item)) {
387
+ return true;
388
+ }
389
+ }
390
+ }
391
+ else if ('type' in child && containsSuspendingAwait(child)) {
392
+ return true;
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+ /**
398
+ * Follows an identifier back to the array literal a `const` binds to it.
399
+ *
400
+ * Only `const` qualifies. A `let`/`var` array can be reassigned between the
401
+ * declaration and the await, so its literal elements are not a sound
402
+ * description of what the aggregate actually received, and treating them as
403
+ * such would suppress reports on genuinely independent operations.
404
+ */
405
+ function resolveConstArrayLiteral(identifier) {
406
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
407
+ if (!variable) {
408
+ return null;
409
+ }
410
+ for (const definition of variable.defs) {
411
+ const declarator = definition.node;
412
+ if (declarator.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
413
+ declarator.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
414
+ declarator.parent.kind === 'const' &&
415
+ declarator.init?.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
416
+ return declarator.init;
417
+ }
418
+ }
419
+ return null;
420
+ }
421
+ /**
422
+ * Expands an awaited promise aggregate (`await Promise.all(ops)`) to the
423
+ * names through which its member promises remain reachable afterwards: the
424
+ * array's own binding plus every element that is a bare identifier, or a
425
+ * spread of one.
426
+ *
427
+ * This is the aliasing channel the plain identifier-set comparison cannot
428
+ * see. In `await Promise.all(ops); await release({ results: [await
429
+ * dropped] })` the two awaits share no identifier at all -- one holds
430
+ * `Promise`/`all`/`ops`, the other `release`/`dropped` -- yet `dropped` is
431
+ * `ops[0]`, so the release genuinely cannot start before the drop settles.
432
+ * Elements that are freshly-constructed promises (`assign()`) are omitted
433
+ * because nothing binds them to a name, so no later await can reference
434
+ * them. (#1541)
435
+ */
436
+ function getAggregatedPromiseNames(awaitExpr) {
437
+ const names = new Set();
438
+ const argument = awaitExpr.argument.type === utils_1.AST_NODE_TYPES.ChainExpression
439
+ ? awaitExpr.argument.expression
440
+ : awaitExpr.argument;
441
+ if (argument.type !== utils_1.AST_NODE_TYPES.CallExpression) {
442
+ return names;
443
+ }
444
+ const callee = argument.callee;
445
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
446
+ callee.computed ||
447
+ callee.object.type !== utils_1.AST_NODE_TYPES.Identifier ||
448
+ callee.object.name !== 'Promise' ||
449
+ callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
450
+ !PROMISE_AGGREGATOR_PATTERN.test(callee.property.name)) {
451
+ return names;
452
+ }
453
+ const [aggregated] = argument.arguments;
454
+ if (!aggregated) {
455
+ return names;
456
+ }
457
+ let elements = [];
458
+ if (aggregated.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
459
+ elements = aggregated.elements;
460
+ }
461
+ else if (aggregated.type === utils_1.AST_NODE_TYPES.Identifier) {
462
+ names.add(aggregated.name);
463
+ elements = resolveConstArrayLiteral(aggregated)?.elements ?? [];
464
+ }
465
+ for (const element of elements) {
466
+ if (!element)
467
+ continue;
468
+ const value = element.type === utils_1.AST_NODE_TYPES.SpreadElement
469
+ ? element.argument
470
+ : element;
471
+ if (value.type === utils_1.AST_NODE_TYPES.Identifier) {
472
+ names.add(value.name);
473
+ }
474
+ }
475
+ return names;
476
+ }
337
477
  /**
338
478
  * Checks if there are dependencies between await expressions
339
479
  */
@@ -486,6 +626,51 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
486
626
  }
487
627
  }
488
628
  }
629
+ // 8. Aggregate-element aliasing barrier. `await Promise.all(ops)` does not
630
+ // consume the promises in `ops`; they stay reachable by name, so a later
631
+ // await that mentions one of them -- `await release({ results: [await
632
+ // dropped] })`, where `dropped` is `ops[0]` -- reads a value the aggregate
633
+ // is still producing. The two awaits share no identifier at all
634
+ // (`Promise`/`all`/`ops` versus `release`/`dropped`), so the direct set
635
+ // comparison above classifies them independent; expanding the aggregate to
636
+ // its element names restores the link. Promise.all-ing that pair would
637
+ // start the consumer concurrently with the very operation whose result it
638
+ // reads. (#1541)
639
+ for (let i = 1; i < awaitNodes.length; i++) {
640
+ const currentIds = allIdentifiers[i];
641
+ if (currentIds.size === 0)
642
+ continue;
643
+ for (let j = 0; j < i; j++) {
644
+ const priorExpr = getAwaitExpression(awaitNodes[j]);
645
+ if (!priorExpr)
646
+ continue;
647
+ for (const aggregatedName of getAggregatedPromiseNames(priorExpr)) {
648
+ if (currentIds.has(aggregatedName)) {
649
+ return true;
650
+ }
651
+ }
652
+ }
653
+ }
654
+ // 9. Nested-await hoist barrier. The rewrite splices each awaited
655
+ // expression into a `Promise.all([...])` ARRAY LITERAL, and array elements
656
+ // evaluate left to right. An element that contains an `await` of its own
657
+ // suspends the enclosing function mid-literal -- after the earlier
658
+ // elements' promises have been constructed, but before `Promise.all` has
659
+ // been called to attach handlers to them. If one of those already-running
660
+ // promises rejects during the suspension, the function throws at the inner
661
+ // await and the rejected promise is orphaned into an `unhandledRejection`,
662
+ // which the Cloud Functions runtime answers by killing the instance, so
663
+ // the caller receives an opaque crash instead of the real error. A leading
664
+ // nested await is unsound in a quieter way: it suspends before the later
665
+ // elements are evaluated, so their operations never start early and the
666
+ // rewrite buys no parallelism while still reshaping the code. Keep any run
667
+ // containing such an expression sequential. (#1541)
668
+ for (const node of awaitNodes) {
669
+ const awaitExpr = getAwaitExpression(node);
670
+ if (awaitExpr && containsSuspendingAwait(awaitExpr.argument)) {
671
+ return true;
672
+ }
673
+ }
489
674
  return false;
490
675
  }
491
676
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.52",
3
+ "version": "1.20.53",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -73,6 +73,7 @@
73
73
  "@types/eslint": "8.37.0",
74
74
  "@types/jest": "29.5.14",
75
75
  "@types/node": "22.20.0",
76
+ "@types/semver": "7.5.8",
76
77
  "@typescript-eslint/eslint-plugin": "5.34.0",
77
78
  "@typescript-eslint/parser": "5.48.0",
78
79
  "chai": "4.5.0",
@@ -103,13 +104,14 @@
103
104
  "remark-lint": "9.1.1",
104
105
  "remark-preset-lint-recommended": "6.1.2",
105
106
  "semantic-release": "25.0.2",
107
+ "semver": "7.7.1",
106
108
  "ts-jest": "29.2.6",
107
109
  "ts-node": "10.9.1",
108
110
  "tsx": "4.19.4",
109
111
  "typescript": "5.0.3"
110
112
  },
111
113
  "peerDependencies": {
112
- "eslint": ">=7"
114
+ "eslint": ">=7 <9"
113
115
  },
114
116
  "engines": {
115
117
  "node": ">=22.0.0"
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.53",
4
+ "date": "2026-08-01T00:55:57.763Z",
5
+ "rules": [
6
+ {
7
+ "name": "parallelize-async-operations",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1541
11
+ ],
12
+ "summary": "stop hoisting dependent and await-containing operations (closes #1541)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.52",
4
18
  "date": "2026-07-31T19:23:46.059Z",