@blumintinc/eslint-plugin-blumint 1.20.163 → 1.20.164

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.163',
226
+ version: '1.20.164',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -234,24 +234,49 @@ function isObjectLikeType(type, checker) {
234
234
  }
235
235
  return 'object';
236
236
  }
237
+ /**
238
+ * Reads through an optional chain to the member access or call it holds.
239
+ * `Object?.keys?.(payload)?.length` parses as a single `ChainExpression`
240
+ * wrapping the whole chain, so a matcher written against a bare
241
+ * `MemberExpression` sees the wrapper and recognizes nothing.
242
+ *
243
+ * Reading through it is sound for the only question asked of it — "is an
244
+ * emptiness check already written here?" — because every optional link guards a
245
+ * nullish RECEIVER, and neither the `Object` global nor the array `Object.keys`
246
+ * hands back is ever nullish. The chained spelling therefore evaluates to
247
+ * exactly what the plain one does, making the two the same guard.
248
+ */
249
+ function unwrapOptionalChain(node) {
250
+ let current = node;
251
+ while (current.type === utils_1.AST_NODE_TYPES.ChainExpression) {
252
+ current = current.expression;
253
+ }
254
+ return current;
255
+ }
237
256
  function isObjectKeysLengthExpression(node, name) {
238
- if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
239
- !node.computed &&
240
- node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
241
- node.property.name === 'length' &&
242
- node.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
243
- node.object.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
244
- !node.object.callee.computed &&
245
- node.object.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
246
- node.object.callee.object.name === 'Object' &&
247
- node.object.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
248
- node.object.callee.property.name === 'keys' &&
249
- node.object.arguments.length === 1 &&
250
- node.object.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier &&
251
- node.object.arguments[0].name === name) {
252
- return true;
257
+ const lengthRead = unwrapOptionalChain(node);
258
+ if (lengthRead.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
259
+ lengthRead.computed ||
260
+ lengthRead.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
261
+ lengthRead.property.name !== 'length') {
262
+ return false;
253
263
  }
254
- return false;
264
+ const keysCall = unwrapOptionalChain(lengthRead.object);
265
+ if (keysCall.type !== utils_1.AST_NODE_TYPES.CallExpression ||
266
+ keysCall.arguments.length !== 1) {
267
+ return false;
268
+ }
269
+ const callee = unwrapOptionalChain(keysCall.callee);
270
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
271
+ callee.computed ||
272
+ callee.object.type !== utils_1.AST_NODE_TYPES.Identifier ||
273
+ callee.object.name !== 'Object' ||
274
+ callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
275
+ callee.property.name !== 'keys') {
276
+ return false;
277
+ }
278
+ const argument = keysCall.arguments[0];
279
+ return argument.type === utils_1.AST_NODE_TYPES.Identifier && argument.name === name;
255
280
  }
256
281
  function isZeroLiteral(node) {
257
282
  return node.type === utils_1.AST_NODE_TYPES.Literal && node.value === 0;
@@ -297,8 +322,18 @@ function conditionHasEmptyCheck(node, name, emptyCheckFunctions, negationDepth =
297
322
  return conditionHasEmptyCheck(node.argument, name, emptyCheckFunctions, negationDepth + 1);
298
323
  }
299
324
  return conditionHasEmptyCheck(node.argument, name, emptyCheckFunctions, negationDepth);
325
+ /**
326
+ * A whole optional chain arrives wrapped, so every arm below — each written
327
+ * against a bare member access or call — is handed a node type it does not
328
+ * match. Delegating to the wrapped expression at the SAME negation depth
329
+ * keeps the wrapper invisible, which is what lets
330
+ * `!Object.keys(data)?.length` and `isEmpty?.(data)` count as the emptiness
331
+ * checks they already are instead of being reported as missing ones.
332
+ */
333
+ case utils_1.AST_NODE_TYPES.ChainExpression:
334
+ return conditionHasEmptyCheck(node.expression, name, emptyCheckFunctions, negationDepth);
300
335
  case utils_1.AST_NODE_TYPES.CallExpression: {
301
- const callee = node.callee;
336
+ const callee = unwrapOptionalChain(node.callee);
302
337
  const firstArgIsTarget = node.arguments[0] &&
303
338
  node.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier &&
304
339
  node.arguments[0].name === name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.163",
3
+ "version": "1.20.164",
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.164",
4
+ "date": "2026-08-17T22:26:37.717Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-empty-object-check",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2034
11
+ ],
12
+ "summary": "see through optional chains (closes #2034)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.163",
4
18
  "date": "2026-08-17T20:02:54.585Z",