@blumintinc/eslint-plugin-blumint 1.20.132 → 1.20.134

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.132',
226
+ version: '1.20.134',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -41,6 +41,23 @@ const DEFAULT_OPTIONS = {
41
41
  '@vitest-environment',
42
42
  ],
43
43
  };
44
+ /**
45
+ * What the consumer's options are deep-merged over, with every null-valued
46
+ * default removed.
47
+ *
48
+ * `applyDefault` merges before `create` runs, and its `deepMerge` classifies
49
+ * `null` as an object (`typeof null === 'object'`), so a key that is null on
50
+ * both sides is recursed into and reaches `Object.keys(null)`. That throws
51
+ * while LOADING the rule, which aborts the lint for the whole file and takes
52
+ * every other rule with it. `headerTemplate` is exactly that shape: `null` is
53
+ * both its schema-legal value and its documented default, so a consumer who
54
+ * writes the documented default out explicitly crashes their own run.
55
+ *
56
+ * Omitting the key leaves the merge nothing to recurse into. Nothing is lost —
57
+ * `normalizeOptions` reads the default straight from `DEFAULT_OPTIONS`, so an
58
+ * absent key and a null one already resolve identically.
59
+ */
60
+ const MERGEABLE_DEFAULT_OPTIONS = Object.fromEntries(Object.entries(DEFAULT_OPTIONS).filter(([, value]) => value !== null));
44
61
  /**
45
62
  * Maximum number of characters to scan at the beginning of a file to detect generated markers.
46
63
  */
@@ -381,7 +398,7 @@ exports.enforceUniqueCursorHeaders = (0, createRule_1.createRule)({
381
398
  splitHeaderFragment: 'Cursor header metadata is split across adjacent comment blocks → Fragmented headers are easy to miss and let required tags drift out of sync → Merge the fragments into a single top-of-file header containing: {{tags}}.',
382
399
  },
383
400
  },
384
- defaultOptions: [DEFAULT_OPTIONS],
401
+ defaultOptions: [MERGEABLE_DEFAULT_OPTIONS],
385
402
  create(context, [userOptions]) {
386
403
  const options = normalizeOptions(userOptions);
387
404
  const fileName = context.getFilename();
@@ -394,6 +394,97 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
394
394
  }
395
395
  return false;
396
396
  }
397
+ /**
398
+ * The array folds whose callback receives the PREVIOUS iteration's result as
399
+ * its first parameter. `reduceRight` carries the same contract as `reduce`;
400
+ * only the traversal order differs, so both fold a chain the same way.
401
+ *
402
+ * `map`/`forEach`/`filter` are deliberately absent: their callbacks receive
403
+ * an ELEMENT first, and awaiting an element is an ordinary data read rather
404
+ * than a serialization point.
405
+ */
406
+ const SEQUENTIAL_FOLD_METHODS = new Set(['reduce', 'reduceRight']);
407
+ /**
408
+ * Reports whether a function node is the callback a fold folds WITH -- the
409
+ * first argument of a `.reduce(...)` / `.reduceRight(...)` call.
410
+ *
411
+ * Only a direct argument qualifies. A callback bound to a name first
412
+ * (`const step = async (promise, doc) => {...}; items.reduce(step, seed)`)
413
+ * is left alone rather than chased through the binding, which keeps the
414
+ * barrier to the shape it can prove.
415
+ */
416
+ function isFoldCallback(fn) {
417
+ const call = fn.parent;
418
+ if (!call ||
419
+ call.type !== utils_1.AST_NODE_TYPES.CallExpression ||
420
+ call.arguments[0] !== fn) {
421
+ return false;
422
+ }
423
+ const callee = call.callee;
424
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
425
+ return false;
426
+ }
427
+ const property = callee.property;
428
+ const methodName = callee.computed
429
+ ? property.type === utils_1.AST_NODE_TYPES.Literal &&
430
+ typeof property.value === 'string'
431
+ ? property.value
432
+ : null
433
+ : property.type === utils_1.AST_NODE_TYPES.Identifier
434
+ ? property.name
435
+ : null;
436
+ return !!methodName && SEQUENTIAL_FOLD_METHODS.has(methodName);
437
+ }
438
+ /**
439
+ * Reports whether a parameter binding is the FIRST parameter of a function
440
+ * -- the accumulator slot of a fold callback.
441
+ *
442
+ * Keyed on position rather than on the binding's name, because the
443
+ * accumulator is spelled `promise`, `acc`, `previous`, `prev`, `chain` or
444
+ * anything else the author preferred, and matching a name list would both
445
+ * miss the unlisted spellings and fire on an element parameter that happens
446
+ * to be called `promise`. A default (`async (acc = Promise.resolve(), doc)`)
447
+ * still binds the accumulator first, so the pattern is unwrapped.
448
+ */
449
+ function isAccumulatorParameter(fn, binding) {
450
+ if (!FUNCTION_BOUNDARY_TYPES.has(fn.type)) {
451
+ return false;
452
+ }
453
+ const [first] = fn.params;
454
+ if (!first) {
455
+ return false;
456
+ }
457
+ const target = first.type === utils_1.AST_NODE_TYPES.AssignmentPattern ? first.left : first;
458
+ return target === binding;
459
+ }
460
+ /**
461
+ * Reports whether an awaited expression is a bare read of the ACCUMULATOR
462
+ * parameter of an enclosing fold callback.
463
+ *
464
+ * The parameter is resolved through the scope chain rather than matched by
465
+ * name, so a local that merely shadows the accumulator's spelling
466
+ * (`const promise = fetchThing(); await promise;` inside the callback) is
467
+ * correctly NOT the accumulator, and an accumulator under any spelling is.
468
+ * TS-only wrappers are unwrapped because `await acc!` and
469
+ * `await acc as Promise<void>` read the same binding.
470
+ */
471
+ function isFoldAccumulatorAwait(awaitExpr) {
472
+ let argument = awaitExpr.argument;
473
+ while (argument.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
474
+ argument.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
475
+ argument = argument.expression;
476
+ }
477
+ if (argument.type !== utils_1.AST_NODE_TYPES.Identifier) {
478
+ return false;
479
+ }
480
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, argument), argument.name);
481
+ if (!variable) {
482
+ return false;
483
+ }
484
+ return variable.defs.some((definition) => definition.type === utils_1.TSESLint.Scope.DefinitionType.Parameter &&
485
+ isAccumulatorParameter(definition.node, definition.name) &&
486
+ isFoldCallback(definition.node));
487
+ }
397
488
  /**
398
489
  * Follows an identifier back to the array literal a `const` binds to it.
399
490
  *
@@ -838,6 +929,27 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
838
929
  }
839
930
  }
840
931
  }
932
+ // 11. Fold-accumulator serialization barrier. `items.reduce(async
933
+ // (promise, item) => { await promise; await store(item); },
934
+ // Promise.resolve())` is the canonical idiom for FORCING sequential
935
+ // execution over a collection: the callback's first parameter IS the
936
+ // previous iteration's completion, so `await promise` is a serialization
937
+ // point rather than an operation of its own. The dependency it expresses
938
+ // is a sequencing one -- the accumulator and `store(item)` share no value
939
+ // at all -- so the identifier comparison above classifies the pair
940
+ // independent and the rewrite hoists the awaits into a Promise.all,
941
+ // starting every iteration's work at once and discarding the exact
942
+ // guarantee the idiom was written to provide. The run is deliberate, not
943
+ // a latency mistake, so the rule declines to report it rather than merely
944
+ // declining the fix. Only an accumulator await with something AFTER it
945
+ // qualifies, mirroring the guard barrier above: it is what follows the
946
+ // barrier that the rewrite would illegally start early. (#1851)
947
+ for (let i = 0; i < awaitNodes.length - 1; i++) {
948
+ const awaitExpr = getAwaitExpression(awaitNodes[i]);
949
+ if (awaitExpr && isFoldAccumulatorAwait(awaitExpr)) {
950
+ return true;
951
+ }
952
+ }
841
953
  return false;
842
954
  }
843
955
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.132",
3
+ "version": "1.20.134",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.20.134",
4
+ "date": "2026-08-07T15:09:56.261Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-unique-cursor-headers",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1853
11
+ ],
12
+ "summary": "keep null defaults out of the options deep-merge (closes #1853)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.133",
18
+ "date": "2026-08-07T14:16:50.604Z",
19
+ "rules": [
20
+ {
21
+ "name": "parallelize-async-operations",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1851
25
+ ],
26
+ "summary": "treat a fold accumulator await as a sequencing barrier (closes #1851)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.132",
4
32
  "date": "2026-08-07T12:26:09.881Z",