@blumintinc/eslint-plugin-blumint 1.19.25 → 1.19.27

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
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.25',
225
+ version: '1.19.27',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -267,7 +267,7 @@ module.exports = {
267
267
  '@blumintinc/blumint/no-async-foreach': 'error',
268
268
  '@blumintinc/blumint/no-console-error': [
269
269
  'error',
270
- { allowWithUseAlertDialog: true },
270
+ { allowWithUseAlertDialog: true, allowErrorInstanceArgument: true },
271
271
  ],
272
272
  '@blumintinc/blumint/no-conditional-literals-in-jsx': 'error',
273
273
  '@blumintinc/blumint/no-filter-without-return': 'error',
@@ -3,6 +3,7 @@ type Options = [
3
3
  {
4
4
  ignorePatterns?: string[];
5
5
  allowWithUseAlertDialog?: boolean;
6
+ allowErrorInstanceArgument?: boolean;
6
7
  }
7
8
  ];
8
9
  export declare const noConsoleError: TSESLint.RuleModule<"noConsoleError", Options, TSESLint.RuleListener>;
@@ -113,6 +113,18 @@ const getLocalNameFromPattern = (target) => {
113
113
  return null;
114
114
  };
115
115
  const findDeclaredVariableByName = (name, variables) => variables.find((variable) => variable.name === name);
116
+ /**
117
+ * A `new <Something>Error(...)` argument is the structured-error handoff the
118
+ * rule exists to encourage: on the backend, `console.error(new HttpsError(...))`
119
+ * is the monitored pipeline (spyOnConsoleErrors reports the Error among the
120
+ * args), not a bypass of it. Detection stays purely syntactic — the rule is not
121
+ * type-aware and runs on files outside any `parserOptions.project` — so a bare
122
+ * identifier (`console.error(error)`) is deliberately excluded because syntax
123
+ * alone cannot prove it holds an Error.
124
+ */
125
+ const isStructuredErrorArgument = (arg) => arg.type === utils_1.AST_NODE_TYPES.NewExpression &&
126
+ arg.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
127
+ /Error$/.test(arg.callee.name);
116
128
  /**
117
129
  * Tracks aliases of `console` and `console.error` to detect indirect calls.
118
130
  */
@@ -451,6 +463,7 @@ exports.noConsoleError = (0, createRule_1.createRule)({
451
463
  items: { type: 'string' },
452
464
  },
453
465
  allowWithUseAlertDialog: { type: 'boolean' },
466
+ allowErrorInstanceArgument: { type: 'boolean' },
454
467
  },
455
468
  additionalProperties: false,
456
469
  },
@@ -467,6 +480,7 @@ exports.noConsoleError = (0, createRule_1.createRule)({
467
480
  if (shouldIgnoreFile(filename))
468
481
  return {};
469
482
  const allowWithUseAlertDialog = options.allowWithUseAlertDialog === true;
483
+ const allowErrorInstanceArgument = options.allowErrorInstanceArgument === true;
470
484
  const aliasTracker = new AliasTracker(context);
471
485
  const dialogTracker = new UseAlertDialogTracker(context);
472
486
  const pendingTracker = new PendingCallTracker();
@@ -511,6 +525,14 @@ exports.noConsoleError = (0, createRule_1.createRule)({
511
525
  const scope = getScopeForNode(context, node);
512
526
  if (!aliasTracker.isConsoleErrorCall(node, scope))
513
527
  return;
528
+ // A structured-error argument is the sanctioned, monitored path, so the
529
+ // carve-out applies to every matched call shape (direct, aliased,
530
+ // destructured, computed) — unlike the useAlertDialog queueing below,
531
+ // which is restricted to the direct-global shape.
532
+ if (allowErrorInstanceArgument &&
533
+ node.arguments.some(isStructuredErrorArgument)) {
534
+ return;
535
+ }
514
536
  // Queueing is limited to direct global `console.error(...)` calls because deferral relies on
515
537
  // linear control flow. Aliased or destructured references can escape the current scope
516
538
  // (stored, passed, invoked later), so they are reported immediately to avoid missing non-local calls.
@@ -178,6 +178,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
178
178
  * name.
179
179
  */
180
180
  const GUARD_PATTERN = /^(assert|ensure|require|validate|verify|guard|check)/i;
181
+ /**
182
+ * Matches state re-read callees by their leading verb. A refetch/refresh
183
+ * that follows a preceding await exists to re-observe state a prior await
184
+ * may have mutated (e.g. `await unlinkProvider(...)` then `await
185
+ * refreshUser()`, where refreshUser re-reads the post-unlink server state).
186
+ * Running such a pair through Promise.all races the refetch ahead of / in
187
+ * parallel with the mutation, so a refresh that resolves first repopulates
188
+ * the just-mutated state -- a genuine correctness bug. Anchored at the
189
+ * start so it fires on the callee's own verb (refreshUser, reloadData,
190
+ * refetchProfile) rather than on an arbitrary substring elsewhere in the
191
+ * name (getRefreshToken must NOT match).
192
+ */
193
+ const REFETCH_PATTERN = /^(refresh|reload|refetch|revalidate|resync|sync)/i;
181
194
  /**
182
195
  * Extracts the callee's method name (the identifier bearing the leading
183
196
  * verb) from an await expression argument. Handles both direct
@@ -324,6 +337,33 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
324
337
  return true;
325
338
  }
326
339
  }
340
+ // 5. Refetch/refresh ordering barrier. A discarded-result await whose
341
+ // callee reads as a state re-read (refresh*, reload*, refetch*,
342
+ // revalidate*, resync*, sync*) exists to observe state that a PRECEDING
343
+ // await may have mutated. Promise.all invokes every operand eagerly and
344
+ // concurrently, so it would race the refetch ahead of the mutation -- a
345
+ // refresh that resolves first repopulates the just-mutated state, a
346
+ // genuine correctness bug. Treat a refetch that FOLLOWS another await as
347
+ // a sequencing dependency that blocks parallelizing the run. Only
348
+ // non-first positions qualify: the preceding await is what the refetch
349
+ // depends on. getCalleeMethodName handles both bare-identifier and
350
+ // member-expression callees. Only discarded-result awaits
351
+ // (ExpressionStatements) qualify here; a captured result carries a value
352
+ // and is handled by the data-dependency path above.
353
+ for (let i = 1; i < awaitNodes.length; i++) {
354
+ const node = awaitNodes[i];
355
+ if (node.type !== utils_1.AST_NODE_TYPES.ExpressionStatement) {
356
+ continue;
357
+ }
358
+ const awaitExpr = getAwaitExpression(node);
359
+ if (!awaitExpr) {
360
+ continue;
361
+ }
362
+ const methodName = getCalleeMethodName(awaitExpr);
363
+ if (methodName && REFETCH_PATTERN.test(methodName)) {
364
+ return true;
365
+ }
366
+ }
327
367
  // If any node is a variable declaration with destructuring, consider it as having dependencies
328
368
  for (const node of awaitNodes) {
329
369
  if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.25",
3
+ "version": "1.19.27",
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.19.27",
4
+ "date": "2026-07-23T04:26:05.419Z",
5
+ "rules": [
6
+ {
7
+ "name": "parallelize-async-operations",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1334
11
+ ],
12
+ "summary": "treat mutation→refetch bare-identifier awaits as ordered (closes #1334)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.19.26",
18
+ "date": "2026-07-23T00:25:08.850Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-console-error",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1333
25
+ ],
26
+ "summary": "allow structured Error argument via allowErrorInstanceArgument (closes #1333)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.19.25",
4
32
  "date": "2026-07-22T21:39:17.787Z",