@blumintinc/eslint-plugin-blumint 1.18.13 → 1.18.15

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
@@ -218,7 +218,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
218
218
  module.exports = {
219
219
  meta: {
220
220
  name: '@blumintinc/eslint-plugin-blumint',
221
- version: '1.18.13',
221
+ version: '1.18.15',
222
222
  },
223
223
  parseOptions: {
224
224
  ecmaVersion: 2020,
@@ -65,9 +65,7 @@ function unwrapExpression(expr) {
65
65
  }
66
66
  return current;
67
67
  }
68
- function getObjectUsagesInHook(hookBody, objectName,
69
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
- context) {
68
+ function getObjectUsagesInHook(hookBody, objectName) {
71
69
  const usages = new Map(); // Track usage and its position
72
70
  const visited = new Set();
73
71
  let needsEntireObject = false;
@@ -133,8 +131,15 @@ context) {
133
131
  const memberExpr = current;
134
132
  // Handle computed properties (like array indices)
135
133
  if (memberExpr.computed) {
136
- // For computed properties with variables (like user[key]), we need the entire object
137
- if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Identifier) {
134
+ // why: only a *literal* computed key (obj[0], obj['special-key'])
135
+ // narrows to a single, stable field. EVERY other computed key —
136
+ // Identifier (obj[i]), CallExpression (obj[assertSafe(i)]),
137
+ // BinaryExpression (obj[i+1]), MemberExpression (obj[keys[j]]),
138
+ // TSAsExpression (obj[k as K]), TemplateLiteral (obj[`row-${i}`]), etc.
139
+ // — is a dynamic access that can read arbitrary elements across an
140
+ // iteration. There is no single narrowable field, so the whole object
141
+ // is a legitimate dependency: resolve the base and mark it accordingly.
142
+ if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Literal) {
138
143
  // Check if this is accessing our target object
139
144
  let currentBase = unwrapExpression(memberExpr.object);
140
145
  while (currentBase.type === utils_1.AST_NODE_TYPES.MemberExpression) {
@@ -142,35 +147,23 @@ context) {
142
147
  }
143
148
  if (currentBase.type === utils_1.AST_NODE_TYPES.Identifier &&
144
149
  currentBase.name === objectName) {
145
- // This is a computed property access on our target object, so we need the entire object
150
+ // This is a dynamic computed property access on our target object,
151
+ // so we need the entire object (no narrowable field exists).
146
152
  needsEntireObject = true;
147
153
  }
148
154
  return null;
149
155
  }
150
156
  // For computed properties with literals
151
- if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Literal) {
152
- const literalProp = memberExpr.property;
153
- if (typeof literalProp.value === 'number') {
154
- parts.unshift(`[${literalProp.value}]`);
155
- }
156
- else if (typeof literalProp.value === 'string') {
157
- parts.unshift(`[${JSON.stringify(literalProp.value)}]`);
158
- }
159
- else {
160
- // For other computed properties, use a wildcard
161
- parts.unshift('[*]');
162
- }
157
+ const literalProp = memberExpr.property;
158
+ if (typeof literalProp.value === 'number') {
159
+ parts.unshift(`[${literalProp.value}]`);
160
+ }
161
+ else if (typeof literalProp.value === 'string') {
162
+ parts.unshift(`[${JSON.stringify(literalProp.value)}]`);
163
163
  }
164
164
  else {
165
- // For other computed properties, use the exact expression
166
- try {
167
- const propertyText = context.sourceCode.getText(memberExpr.property);
168
- parts.unshift(`[${propertyText}]`);
169
- }
170
- catch (e) {
171
- // Fallback to wildcard if we can't get the source text
172
- parts.unshift('[*]');
173
- }
165
+ // For other literal computed properties (e.g. boolean/null), wildcard
166
+ parts.unshift('[*]');
174
167
  }
175
168
  }
176
169
  else {
@@ -184,7 +177,7 @@ context) {
184
177
  STRING_METHODS.has(memberExpr.property.name))) {
185
178
  // Check if this is accessing our target object or a property of it
186
179
  let currentBase = unwrapExpression(memberExpr.object);
187
- let pathParts = [];
180
+ const pathParts = [];
188
181
  let hasOptionalChainingInMethod = false;
189
182
  // Build the path to the array/string being accessed
190
183
  while (currentBase.type === utils_1.AST_NODE_TYPES.MemberExpression) {
@@ -352,7 +345,8 @@ context) {
352
345
  effectiveParent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression)) {
353
346
  effectiveParent = effectiveParent.parent;
354
347
  }
355
- const isIntermediate = effectiveParent && effectiveParent.type === utils_1.AST_NODE_TYPES.MemberExpression;
348
+ const isIntermediate = effectiveParent &&
349
+ effectiveParent.type === utils_1.AST_NODE_TYPES.MemberExpression;
356
350
  if (!isIntermediate) {
357
351
  // Check if this member expression involves our target object
358
352
  let current = memberExpr;
@@ -361,9 +355,12 @@ context) {
361
355
  // Walk up the member expression chain to see if it involves our target object
362
356
  while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
363
357
  const currentMember = current;
364
- // Check if this level uses dynamic computed property access
358
+ // Check if this level uses dynamic computed property access.
359
+ // why: any non-literal computed key reads arbitrary elements, so it
360
+ // requires the entire object. Only a literal key (obj[0], obj['k'])
361
+ // narrows to a single, stable field.
365
362
  if (currentMember.computed &&
366
- currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
363
+ currentMember.property.type !== utils_1.AST_NODE_TYPES.Literal) {
367
364
  hasDynamicComputed = true;
368
365
  }
369
366
  current = unwrapExpression(currentMember.object);
@@ -582,7 +579,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
582
579
  }
583
580
  }
584
581
  // For testing without TypeScript services, we'll assume all identifiers are objects
585
- const result = getObjectUsagesInHook(callbackArg.body, objectName, context);
582
+ const result = getObjectUsagesInHook(callbackArg.body, objectName);
586
583
  // If the object is not used at all, suggest removing it
587
584
  if (result.notUsed) {
588
585
  context.report({
@@ -100,11 +100,18 @@ function isHookName(name) {
100
100
  }
101
101
  /**
102
102
  * Detects PascalCase identifiers commonly used for React components.
103
+ *
104
+ * SCREAMING_SNAKE_CASE names (`CHANNEL_OPTIONS`, `MAX_COUNT`, `SPACING`) are
105
+ * constants by convention, not components, so they are excluded even though
106
+ * they begin with an uppercase letter. Treating them as components would
107
+ * misclassify a module-scope constant's `.map`/`.filter` callback body as a
108
+ * render body and flag literals that are only ever built once at import.
103
109
  * @param name Candidate identifier name.
104
- * @returns True when the name begins with an uppercase character.
110
+ * @returns True when the name is PascalCase (starts uppercase and contains a
111
+ * lowercase letter), false for all-caps constants.
105
112
  */
106
113
  function isComponentName(name) {
107
- return !!name && /^[A-Z]/.test(name);
114
+ return !!name && /^[A-Z]/.test(name) && !/^[A-Z][A-Z0-9_]*$/.test(name);
108
115
  }
109
116
  /**
110
117
  * True when `node` is a function literal passed directly as an argument to an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.18.13",
3
+ "version": "1.18.15",
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.18.15",
4
+ "date": "2026-07-13T15:25:03.959Z",
5
+ "rules": [
6
+ {
7
+ "name": "react-memoize-literals",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1292
11
+ ],
12
+ "summary": "exclude SCREAMING_SNAKE_CASE constants from component detection (closes #1292)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.18.14",
18
+ "date": "2026-07-13T08:30:23.444Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-entire-object-hook-deps",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1291
25
+ ],
26
+ "summary": "treat non-literal computed keys as whole-object access (closes #1291)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.18.13",
4
32
  "date": "2026-07-13T01:43:11.918Z",