@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 +1 -1
- package/lib/rules/enforce-empty-object-check.js +52 -17
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|