@blumintinc/eslint-plugin-blumint 1.20.70 → 1.20.71

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.70',
226
+ version: '1.20.71',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -43,6 +43,42 @@ const isParenthesizedType = (node) => {
43
43
  return (candidate.type === 'TSParenthesizedType' &&
44
44
  candidate.typeAnnotation !== undefined);
45
45
  };
46
+ const unwrapParenthesizedTypeNode = (node) => {
47
+ let current = node;
48
+ // Cap iterations for the same reason as unwrapArrayElementType: wrappers are
49
+ // finite, but a future wrapper case must not be able to loop forever.
50
+ for (let i = 0; i < 10; i++) {
51
+ if (isParenthesizedType(current)) {
52
+ current = current.typeAnnotation;
53
+ continue;
54
+ }
55
+ break;
56
+ }
57
+ return current;
58
+ };
59
+ // Assertion wrappers never change the runtime value, so `[...] as const` and
60
+ // `[...] as const satisfies readonly string[]` both still describe an array.
61
+ const EXPRESSION_ASSERTION_TYPES = new Set([
62
+ utils_1.AST_NODE_TYPES.TSAsExpression,
63
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
64
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
65
+ 'TSSatisfiesExpression',
66
+ ]);
67
+ const unwrapExpressionAssertions = (node) => {
68
+ let current = node;
69
+ for (let i = 0; i < 10; i++) {
70
+ if (EXPRESSION_ASSERTION_TYPES.has(current.type)) {
71
+ const inner = current
72
+ .expression;
73
+ if (!inner)
74
+ break;
75
+ current = inner;
76
+ continue;
77
+ }
78
+ break;
79
+ }
80
+ return current;
81
+ };
46
82
  const unwrapArrayElementType = (node) => {
47
83
  let current = node;
48
84
  // Fixpoint loop: peel wrappers in any order until none remain
@@ -181,6 +217,7 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
181
217
  const aliasNameToType = new Map();
182
218
  const interfaceNames = new Set();
183
219
  const enumNames = new Set();
220
+ const constArrayNameToLiteral = new Map();
184
221
  const visitNode = (n) => {
185
222
  switch (n.type) {
186
223
  case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration: {
@@ -195,6 +232,22 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
195
232
  enumNames.add(n.id.name);
196
233
  break;
197
234
  }
235
+ case utils_1.AST_NODE_TYPES.VariableDeclaration: {
236
+ // Only `const` bindings can back a `(typeof X)[number]` element union
237
+ if (n.kind !== 'const')
238
+ break;
239
+ for (const declarator of n.declarations) {
240
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
241
+ !declarator.init) {
242
+ continue;
243
+ }
244
+ const init = unwrapExpressionAssertions(declarator.init);
245
+ if (init.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
246
+ constArrayNameToLiteral.set(declarator.id.name, init);
247
+ }
248
+ }
249
+ break;
250
+ }
198
251
  case utils_1.AST_NODE_TYPES.ExportNamedDeclaration: {
199
252
  if (n.declaration)
200
253
  visitNode(n.declaration);
@@ -235,6 +288,85 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
235
288
  }
236
289
  const seenAlias = new Set();
237
290
  const visitingAliases = new Set();
291
+ const isPrimitiveLiteralElement = (element, visitedConstArrays) => {
292
+ // Array holes resolve to `undefined`, but the shape is unusual enough
293
+ // that refusing to classify it keeps the narrowing conservative.
294
+ if (!element)
295
+ return false;
296
+ if (element.type === utils_1.AST_NODE_TYPES.SpreadElement) {
297
+ const argument = unwrapExpressionAssertions(element.argument);
298
+ if (argument.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
299
+ return argument.elements.every((nested) => isPrimitiveLiteralElement(nested, visitedConstArrays));
300
+ }
301
+ if (argument.type === utils_1.AST_NODE_TYPES.Identifier) {
302
+ return isPrimitiveConstArray(argument.name, visitedConstArrays);
303
+ }
304
+ return false;
305
+ }
306
+ const expression = unwrapExpressionAssertions(element);
307
+ switch (expression.type) {
308
+ case utils_1.AST_NODE_TYPES.Literal: {
309
+ // A regex literal is an object; its `value` is engine-dependent, so
310
+ // reject it explicitly rather than relying on the typeof check.
311
+ if (expression.regex)
312
+ return false;
313
+ const value = expression.value;
314
+ return (value === null ||
315
+ typeof value === 'string' ||
316
+ typeof value === 'number' ||
317
+ typeof value === 'boolean' ||
318
+ typeof value === 'bigint');
319
+ }
320
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
321
+ return true; // a template literal always produces a string
322
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
323
+ // Nested primitive arrays mirror the allowance for `string[][]` and
324
+ // tuples of primitives elsewhere in this rule.
325
+ return expression.elements.every((nested) => isPrimitiveLiteralElement(nested, visitedConstArrays));
326
+ case utils_1.AST_NODE_TYPES.UnaryExpression: {
327
+ const unary = expression;
328
+ if (unary.operator !== '-' && unary.operator !== '+')
329
+ return false;
330
+ return isPrimitiveLiteralElement(unary.argument, visitedConstArrays);
331
+ }
332
+ case utils_1.AST_NODE_TYPES.Identifier:
333
+ return expression.name === 'undefined';
334
+ default:
335
+ return false;
336
+ }
337
+ };
338
+ const isPrimitiveConstArray = (name, visitedConstArrays) => {
339
+ // A cyclic spread cannot be resolved syntactically; refuse to classify it
340
+ if (visitedConstArrays.has(name))
341
+ return false;
342
+ const arrayLiteral = constArrayNameToLiteral.get(name);
343
+ if (!arrayLiteral)
344
+ return false;
345
+ visitedConstArrays.add(name);
346
+ const result = arrayLiteral.elements.every((element) => isPrimitiveLiteralElement(element, visitedConstArrays));
347
+ visitedConstArrays.delete(name);
348
+ return result;
349
+ };
350
+ /**
351
+ * Recognizes `(typeof VALUES)[number]` where VALUES is a same-file const
352
+ * array of primitive literals. That form denotes the union of those
353
+ * literals, not an object lookup, and it is exactly what the sibling
354
+ * rule prefer-union-from-const-array autofixes toward.
355
+ */
356
+ const isConstArrayElementUnion = (node) => {
357
+ const indexType = unwrapParenthesizedTypeNode(node.indexType);
358
+ // Only a `number` index yields the element union; `['length']` or any key
359
+ // lookup resolves to something this syntactic check cannot vouch for.
360
+ if (indexType.type !== utils_1.AST_NODE_TYPES.TSNumberKeyword)
361
+ return false;
362
+ const objectType = unwrapParenthesizedTypeNode(node.objectType);
363
+ if (objectType.type !== utils_1.AST_NODE_TYPES.TSTypeQuery)
364
+ return false;
365
+ const exprName = objectType.exprName;
366
+ if (exprName.type !== utils_1.AST_NODE_TYPES.Identifier)
367
+ return false;
368
+ return isPrimitiveConstArray(exprName.name, new Set());
369
+ };
238
370
  const isPrimitiveLikeAlias = (name, recursionDepth) => {
239
371
  if (seenAlias.has(name))
240
372
  return true;
@@ -286,6 +418,8 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
286
418
  return false;
287
419
  case utils_1.AST_NODE_TYPES.TSLiteralType:
288
420
  return true; // string/number/boolean literals
421
+ case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
422
+ return isConstArrayElementUnion(node);
289
423
  case utils_1.AST_NODE_TYPES.TSTypeReference: {
290
424
  // Allow known primitive-like references and enums or primitive-like aliases
291
425
  const ref = node;
@@ -361,8 +495,10 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
361
495
  case utils_1.AST_NODE_TYPES.TSMappedType:
362
496
  return true;
363
497
  case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
364
- // Treat indexed access as object-like to align with existing tests
365
- return true;
498
+ // An indexed access such as `DataShape['user']` is an object lookup,
499
+ // but `(typeof VALUES)[number]` over a const array of primitive
500
+ // literals is a primitive union and must not be flagged.
501
+ return !isConstArrayElementUnion(node);
366
502
  case utils_1.AST_NODE_TYPES.TSTypeOperator:
367
503
  if (node.operator === 'readonly') {
368
504
  return isObjectType(node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.70",
3
+ "version": "1.20.71",
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.71",
4
+ "date": "2026-08-02T01:25:22.489Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-firestore-object-arrays",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1594
11
+ ],
12
+ "summary": "treat (typeof X)[number] over a primitive const array as a primitive union (closes #1594)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.70",
4
18
  "date": "2026-08-01T23:31:02.214Z",