@blumintinc/eslint-plugin-blumint 1.20.45 → 1.20.46

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.45',
226
+ version: '1.20.46',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -61,7 +61,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
61
61
  guardFunctions: {
62
62
  type: 'array',
63
63
  items: { type: 'string' },
64
- description: 'Canonical type guard function names',
64
+ description: 'Type guard function names; the first is the one suggestions call and import',
65
65
  },
66
66
  excludeFiles: {
67
67
  type: 'array',
@@ -70,23 +70,32 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
70
70
  },
71
71
  guardImportSource: {
72
72
  type: 'string',
73
- description: 'Module specifier the suggestion imports isSnapshotReady from',
73
+ description: 'Module specifier the suggestion imports the guard from',
74
74
  },
75
75
  },
76
76
  additionalProperties: false,
77
77
  },
78
78
  ],
79
79
  messages: {
80
- noFalsyCheck: "Do not use boolean coercion on FirestoreSnapshotState<T>. All string states ('idle', 'loading', 'not-found') are truthy, so '{{expression}}' does not behave as intended. Use isSnapshotReady(state) to narrow to T, or compare explicitly (e.g., state === 'loading').",
81
- noRawTypeof: "Do not use '{{expression}}' to narrow FirestoreSnapshotState<T> to data. Use isSnapshotReady(state) instead to maintain the abstraction boundary.",
80
+ // `guard` is the configured canonical guard name, so a consumer who
81
+ // renames it is not told to call a function their config says does not
82
+ // exist. Every report and suggestion supplies it.
83
+ noFalsyCheck: "Do not use boolean coercion on FirestoreSnapshotState<T>. All string states ('idle', 'loading', 'not-found') are truthy, so '{{expression}}' does not behave as intended. Use {{guard}}(state) to narrow to T, or compare explicitly (e.g., state === 'loading').",
84
+ noRawTypeof: "Do not use '{{expression}}' to narrow FirestoreSnapshotState<T> to data. Use {{guard}}(state) instead to maintain the abstraction boundary.",
82
85
  },
83
86
  },
84
87
  defaultOptions: [{}],
85
88
  create(context, [options]) {
86
89
  const snapshotHooks = new Set(options?.snapshotHooks ?? DEFAULT_SNAPSHOT_HOOKS);
87
- // guardFunctions is accepted in config for documentation and future extensibility
88
- // but detection is purely syntactic (by hook source), not by guard function name.
89
- void (options?.guardFunctions ?? DEFAULT_GUARD_FUNCTIONS);
90
+ /**
91
+ * The name every suggestion calls, resolves in scope, and imports.
92
+ * `guardFunctions` may list several recognized guards; the first usable one
93
+ * is canonical. A list that names nothing callable — empty, or holding only
94
+ * blanks — falls back to the default rather than leaving the suggestion to
95
+ * emit `undefined(state)` or `(state)`.
96
+ */
97
+ const guardName = options?.guardFunctions?.find((name) => name.trim().length > 0) ??
98
+ DEFAULT_GUARD_FUNCTIONS[0];
90
99
  const excludeFiles = options?.excludeFiles ?? [
91
100
  'src/types/FirestoreSnapshotState.ts',
92
101
  ];
@@ -163,7 +172,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
163
172
  * import, or would collide with an unrelated binding of the same name.
164
173
  */
165
174
  function resolveGuardBinding(scope) {
166
- const variable = utils_1.ASTUtils.findVariable(scope, GUARD_NAME);
175
+ const variable = utils_1.ASTUtils.findVariable(scope, guardName);
167
176
  if (!variable) {
168
177
  return 'missing';
169
178
  }
@@ -214,14 +223,14 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
214
223
  if (reusable) {
215
224
  const namedSpecifiers = reusable.specifiers.filter(isValueImportSpecifier);
216
225
  const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
217
- return fixer.insertTextAfter(lastSpecifier, `, ${GUARD_NAME}`);
226
+ return fixer.insertTextAfter(lastSpecifier, `, ${guardName}`);
218
227
  }
219
228
  // A namespace or type-only import of the module cannot take a named value
220
229
  // specifier, but its path is proof of how this file reaches the module.
221
230
  const source = guardDeclarations.length
222
231
  ? String(guardDeclarations[0].source.value)
223
232
  : guardImportSource;
224
- const importText = `import { ${GUARD_NAME} } from '${source}';\n`;
233
+ const importText = `import { ${guardName} } from '${source}';\n`;
225
234
  const [firstImport] = declarations;
226
235
  if (firstImport) {
227
236
  return fixer.insertTextBefore(firstImport, importText);
@@ -233,7 +242,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
233
242
  }
234
243
  /**
235
244
  * Builds the single suggestion shared by every report: swap the flagged
236
- * expression for its guard-based equivalent and bring `isSnapshotReady` into
245
+ * expression for its guard-based equivalent and bring the guard into
237
246
  * scope. Declines (no suggestion) when the name is already taken by
238
247
  * something that is not the guard.
239
248
  */
@@ -241,7 +250,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
241
250
  return [
242
251
  {
243
252
  messageId,
244
- data: { expression: replacement },
253
+ data: { expression: replacement, guard: guardName },
245
254
  fix(fixer) {
246
255
  const binding = resolveGuardBinding(scope);
247
256
  if (binding === 'conflict') {
@@ -306,8 +315,8 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
306
315
  context.report({
307
316
  node,
308
317
  messageId: 'noRawTypeof',
309
- data: { expression: getText(node) },
310
- suggest: guardSuggestion('noRawTypeof', node, `${GUARD_NAME}(${operand.name})`, context.getScope()),
318
+ data: { expression: getText(node), guard: guardName },
319
+ suggest: guardSuggestion('noRawTypeof', node, `${guardName}(${operand.name})`, context.getScope()),
311
320
  });
312
321
  }
313
322
  // typeof state === 'string' — allowed (narrows to non-data states)
@@ -327,7 +336,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
327
336
  context.report({
328
337
  node,
329
338
  messageId: 'noFalsyCheck',
330
- data: { expression },
339
+ data: { expression, guard: guardName },
331
340
  suggest: guardSuggestion('noFalsyCheck', fixNode, replacement, context.getScope()),
332
341
  });
333
342
  }
@@ -372,7 +381,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
372
381
  // stay negated or the suggestion would reverse the control flow.
373
382
  if (argument.type === utils_1.AST_NODE_TYPES.Identifier &&
374
383
  isSnapshotVar(argument)) {
375
- reportFalsyCheck(node, `!${argument.name}`, `!${GUARD_NAME}(${argument.name})`);
384
+ reportFalsyCheck(node, `!${argument.name}`, `!${guardName}(${argument.name})`);
376
385
  }
377
386
  // !!state — the argument is another `!` whose argument is the snapshot var.
378
387
  // The double negation is a truthiness coercion, so the guard is positive.
@@ -381,7 +390,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
381
390
  argument.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
382
391
  isSnapshotVar(argument.argument)) {
383
392
  const varName = argument.argument.name;
384
- reportFalsyCheck(node, `!!${varName}`, `${GUARD_NAME}(${varName})`);
393
+ reportFalsyCheck(node, `!!${varName}`, `${guardName}(${varName})`);
385
394
  }
386
395
  },
387
396
  // IfStatement: if (state) { ... } or if (!state) { ... }
@@ -389,14 +398,14 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
389
398
  IfStatement(node) {
390
399
  const test = node.test;
391
400
  if (test.type === utils_1.AST_NODE_TYPES.Identifier && isSnapshotVar(test)) {
392
- reportFalsyCheck(test, test.name, `${GUARD_NAME}(${test.name})`);
401
+ reportFalsyCheck(test, test.name, `${guardName}(${test.name})`);
393
402
  }
394
403
  },
395
404
  // ConditionalExpression: state ? a : b
396
405
  ConditionalExpression(node) {
397
406
  const test = node.test;
398
407
  if (test.type === utils_1.AST_NODE_TYPES.Identifier && isSnapshotVar(test)) {
399
- reportFalsyCheck(test, test.name, `${GUARD_NAME}(${test.name})`);
408
+ reportFalsyCheck(test, test.name, `${guardName}(${test.name})`);
400
409
  }
401
410
  },
402
411
  // LogicalExpression: state && expr, state || expr
@@ -408,13 +417,13 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
408
417
  if (node.operator === '&&') {
409
418
  // `state && expr` guards expr, so swapping the operand for the
410
419
  // guard keeps both the polarity and the narrowing of `state`.
411
- reportFalsyCheck(left, left.name, `${GUARD_NAME}(${left.name})`);
420
+ reportFalsyCheck(left, left.name, `${guardName}(${left.name})`);
412
421
  return;
413
422
  }
414
423
  // `state || fallback` evaluates to the state itself when it is
415
424
  // usable, so a bare operand swap would yield `true` instead of the
416
425
  // data. Only the conditional form preserves that value.
417
- const guarded = `${GUARD_NAME}(${left.name}) ? ${left.name} : ${getText(node.right)}`;
426
+ const guarded = `${guardName}(${left.name}) ? ${left.name} : ${getText(node.right)}`;
418
427
  reportFalsyCheck(left, left.name, needsParentheses(node) ? `(${guarded})` : guarded, node);
419
428
  }
420
429
  },
@@ -432,7 +441,7 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
432
441
  node.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier &&
433
442
  isSnapshotVar(node.arguments[0])) {
434
443
  const varName = node.arguments[0].name;
435
- reportFalsyCheck(node, `Boolean(${varName})`, `${GUARD_NAME}(${varName})`);
444
+ reportFalsyCheck(node, `Boolean(${varName})`, `${guardName}(${varName})`);
436
445
  }
437
446
  },
438
447
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.45",
3
+ "version": "1.20.46",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.46",
4
+ "date": "2026-07-31T07:08:55.415Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-snapshot-state-narrowing",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1505
11
+ ],
12
+ "summary": "make the guardFunctions option actually select the emitted guard name (closes #1505)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.45",
4
18
  "date": "2026-07-31T06:37:56.216Z",