@blumintinc/eslint-plugin-blumint 1.20.126 → 1.20.128
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-assert-safe-object-key.js +62 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +42 -19
- package/lib/rules/enforce-firestore-doc-ref-generic.js +22 -4
- package/lib/rules/enforce-firestore-set-merge.js +30 -2
- package/lib/rules/enforce-querykey-ts.js +10 -0
- package/lib/rules/fast-deep-equal-over-microdiff.js +38 -24
- package/lib/rules/no-direct-function-state.d.ts +2 -1
- package/lib/rules/no-direct-function-state.js +93 -17
- package/lib/rules/no-hungarian.js +161 -2
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -0
- package/lib/rules/no-passthrough-getters.js +176 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +51 -25
- package/lib/rules/prefer-global-router-state-key.js +72 -14
- package/package.json +3 -1
- package/release-manifest.json +109 -0
package/lib/index.js
CHANGED
|
@@ -129,6 +129,10 @@ function isInsideMockFactory(node) {
|
|
|
129
129
|
*
|
|
130
130
|
* The peel repeats because the wrappers nest: `(x as any)!` is a non-null
|
|
131
131
|
* assertion over a type assertion.
|
|
132
|
+
*
|
|
133
|
+
* Everything peeled here erases before the code runs, which is why the peel is
|
|
134
|
+
* unconditional. An optional chain does not, so it is handled apart from these
|
|
135
|
+
* — see `unwrapOptionalChain`.
|
|
132
136
|
*/
|
|
133
137
|
function unwrapKeyExpression(node) {
|
|
134
138
|
let current = node;
|
|
@@ -148,6 +152,43 @@ function unwrapKeyExpression(node) {
|
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Reads through an optional chain to the member access or call it holds.
|
|
157
|
+
* `source?.key` parses as a `ChainExpression` wrapping the member expression,
|
|
158
|
+
* so a classification that matches a bare `MemberExpression` — or a numeric
|
|
159
|
+
* proof that matches `.length` — sees the wrapper and recognizes nothing.
|
|
160
|
+
*
|
|
161
|
+
* Kept apart from `unwrapKeyExpression` rather than folded into it because the
|
|
162
|
+
* two make different claims. Those wrappers are gone before the code runs; `?.`
|
|
163
|
+
* survives and short-circuits, so it is read through only where the question is
|
|
164
|
+
* "what value names this property", never where the question is what the
|
|
165
|
+
* expression does. That value is what the chain evaluates to, `undefined`
|
|
166
|
+
* included — and the chain guards a nullish RECEIVER, not a hostile KEY:
|
|
167
|
+
* `"__proto__"` is a perfectly non-nullish string, so `store[req.body?.key]`
|
|
168
|
+
* reaches the prototype surface exactly as `store[req.body.key]` does.
|
|
169
|
+
*/
|
|
170
|
+
function unwrapOptionalChain(node) {
|
|
171
|
+
return node.type === utils_1.AST_NODE_TYPES.ChainExpression ? node.expression : node;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The key expression stripped of every wrapper standing between it and the
|
|
175
|
+
* value that names the property: the compile-time assertions and the `await`
|
|
176
|
+
* that `unwrapKeyExpression` peels, plus an optional chain.
|
|
177
|
+
*
|
|
178
|
+
* The peel repeats because the two kinds nest in either order — `source?.key as
|
|
179
|
+
* string` is an assertion over a chain, `(source as Raw)?.key` a chain over an
|
|
180
|
+
* assertion.
|
|
181
|
+
*/
|
|
182
|
+
function unwrapWrittenKey(node) {
|
|
183
|
+
let current = node;
|
|
184
|
+
for (;;) {
|
|
185
|
+
const peeled = unwrapOptionalChain(unwrapKeyExpression(current));
|
|
186
|
+
if (peeled === current) {
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
current = peeled;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
151
192
|
/** Names that read as a positional sequence rather than a keyed record. */
|
|
152
193
|
const ARRAY_LIKE_NAME = /^(array|arr|items|elements|list|collection|data)s?$/i;
|
|
153
194
|
/**
|
|
@@ -534,7 +575,8 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
534
575
|
},
|
|
535
576
|
});
|
|
536
577
|
/**
|
|
537
|
-
* Reports a key whose written form may carry assertion or await wrappers
|
|
578
|
+
* Reports a key whose written form may carry assertion or await wrappers or
|
|
579
|
+
* an optional chain.
|
|
538
580
|
*
|
|
539
581
|
* The report and the fix sit on the outermost written node, so the wrapper
|
|
540
582
|
* the author put there survives the rewrite: `m[assertSafe(k as string)]`
|
|
@@ -542,7 +584,10 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
542
584
|
* own. `assertSafe` is identity-typed (`<T extends PropertyKey>(key: T): T`),
|
|
543
585
|
* so wrapping the asserted expression preserves the key's type, and wrapping
|
|
544
586
|
* an `await` keeps the validation on the resolved key rather than moving it
|
|
545
|
-
* onto the promise.
|
|
587
|
+
* onto the promise. Wrapping the whole chain is what keeps the short-circuit
|
|
588
|
+
* intact: `m[assertSafe(source?.key)]` evaluates `source?.key` once, in the
|
|
589
|
+
* position the author wrote it, and hands assertSafe what it produces — the
|
|
590
|
+
* rewrite adds a validation, it does not move a dereference.
|
|
546
591
|
*
|
|
547
592
|
* A key written without a wrapper keeps the narrower argument the fix has
|
|
548
593
|
* always emitted: `String(id)` and `` `${id}` `` collapse to `id`, whose
|
|
@@ -563,8 +608,11 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
563
608
|
if (!variable)
|
|
564
609
|
return false;
|
|
565
610
|
return variable.defs.some((def) => {
|
|
566
|
-
|
|
567
|
-
|
|
611
|
+
// `assertSafe?.(rawKey)` produces the very same validated key as
|
|
612
|
+
// `assertSafe(rawKey)` — the chain guards only a nullish callee — so
|
|
613
|
+
// the exemption reads through it rather than re-reporting the binding.
|
|
614
|
+
const init = def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && def.node.init
|
|
615
|
+
? unwrapOptionalChain(def.node.init)
|
|
568
616
|
: null;
|
|
569
617
|
return (!!init &&
|
|
570
618
|
init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -584,8 +632,11 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
584
632
|
// An assertion or an await around an operand leaves its run-time value
|
|
585
633
|
// alone, so the proof reads through to what the wrapper holds. The
|
|
586
634
|
// annotation on the binding underneath is what proves the key numeric —
|
|
587
|
-
// an assertion asserts and proves nothing on its own.
|
|
588
|
-
|
|
635
|
+
// an assertion asserts and proves nothing on its own. An optional chain
|
|
636
|
+
// is read through as well: `xs?.length` is the same `.length` proof, and
|
|
637
|
+
// its short-circuit yields `undefined`, which stringifies to "undefined"
|
|
638
|
+
// and so still names no field of the prototype surface.
|
|
639
|
+
const target = unwrapWrittenKey(node);
|
|
589
640
|
switch (target.type) {
|
|
590
641
|
case utils_1.AST_NODE_TYPES.Literal:
|
|
591
642
|
return typeof target.value === 'number';
|
|
@@ -662,7 +713,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
662
713
|
Property(node) {
|
|
663
714
|
if (node.computed && node.key) {
|
|
664
715
|
const written = node.key;
|
|
665
|
-
const key =
|
|
716
|
+
const key = unwrapWrittenKey(written);
|
|
666
717
|
// Check for String(id) pattern
|
|
667
718
|
if (key.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
668
719
|
key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -687,7 +738,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
687
738
|
BinaryExpression(node) {
|
|
688
739
|
if (node.operator === 'in') {
|
|
689
740
|
const written = node.left;
|
|
690
|
-
const left =
|
|
741
|
+
const left = unwrapWrittenKey(written);
|
|
691
742
|
// Check for String(id) pattern
|
|
692
743
|
if (left.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
693
744
|
left.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -712,9 +763,9 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
712
763
|
if (node.computed) {
|
|
713
764
|
const written = node.property;
|
|
714
765
|
// The written key may sit under assertion or await wrappers that erase
|
|
715
|
-
// at run time; what they hold is what
|
|
716
|
-
// what the branches below classify.
|
|
717
|
-
const property =
|
|
766
|
+
// at run time, or under an optional chain; what they hold is what
|
|
767
|
+
// names the property, so that is what the branches below classify.
|
|
768
|
+
const property = unwrapWrittenKey(written);
|
|
718
769
|
// Skip if already using assertSafe
|
|
719
770
|
if (property.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
720
771
|
property.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -261,26 +261,49 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
261
261
|
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, callee), 'Boolean');
|
|
262
262
|
return !variable || variable.defs.length === 0;
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* An optional link wraps the member access or call it belongs to in a
|
|
266
|
+
* `ChainExpression`, so `user?.isLoggedIn` and `canDelete?.('x')` reach a
|
|
267
|
+
* value check as a wrapper node rather than as the member/call the check
|
|
268
|
+
* looks for.
|
|
269
|
+
*
|
|
270
|
+
* Unwrapping is the right answer for THIS rule even though `a?.b` is
|
|
271
|
+
* `boolean | undefined` where `a.b` is `boolean`: the rule's remedy is a
|
|
272
|
+
* rename of the binding, which never changes how the initializer
|
|
273
|
+
* short-circuits, and the rule already requires the prefix on
|
|
274
|
+
* possibly-undefined booleans elsewhere — `deletable?: boolean` on a
|
|
275
|
+
* parameter, class property or method all report, and so does
|
|
276
|
+
* `const loggedIn = user && user.isLoggedIn`, whose type is exactly the
|
|
277
|
+
* `boolean | undefined` an optional chain produces. A value that may be
|
|
278
|
+
* absent is where an unprefixed name misleads most, because a falsy result
|
|
279
|
+
* no longer distinguishes "false" from "receiver was missing".
|
|
280
|
+
*/
|
|
281
|
+
function unwrapChainExpression(expression) {
|
|
282
|
+
return expression.type === utils_1.AST_NODE_TYPES.ChainExpression
|
|
283
|
+
? expression.expression
|
|
284
|
+
: expression;
|
|
285
|
+
}
|
|
264
286
|
/**
|
|
265
287
|
* Check if a node is initialized with a boolean value
|
|
266
288
|
*/
|
|
267
289
|
function hasInitialBooleanValue(node) {
|
|
268
290
|
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && node.init) {
|
|
291
|
+
const init = unwrapChainExpression(node.init);
|
|
269
292
|
// Check for direct boolean literal initialization
|
|
270
|
-
if (
|
|
271
|
-
typeof
|
|
293
|
+
if (init.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
294
|
+
typeof init.value === 'boolean') {
|
|
272
295
|
return true;
|
|
273
296
|
}
|
|
274
297
|
// Check for logical expressions that typically return boolean
|
|
275
|
-
if (
|
|
276
|
-
BOOLEANISH_BINARY_OPERATORS.has(
|
|
298
|
+
if (init.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
299
|
+
BOOLEANISH_BINARY_OPERATORS.has(init.operator)) {
|
|
277
300
|
return true;
|
|
278
301
|
}
|
|
279
302
|
// Check for logical expressions (&&)
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
const left = evaluateBooleanishExpression(
|
|
283
|
-
const right = evaluateBooleanishExpression(
|
|
303
|
+
if (init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
304
|
+
init.operator === '&&') {
|
|
305
|
+
const left = evaluateBooleanishExpression(init.left);
|
|
306
|
+
const right = evaluateBooleanishExpression(init.right);
|
|
284
307
|
// If both sides are boolean, the result is boolean.
|
|
285
308
|
if (left === 'boolean' && right === 'boolean') {
|
|
286
309
|
return true;
|
|
@@ -297,10 +320,10 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
297
320
|
// Special case for logical OR (||) - only consider it boolean if:
|
|
298
321
|
// 1. It's used with boolean literals or
|
|
299
322
|
// 2. It's not used with array/object literals as fallbacks
|
|
300
|
-
if (
|
|
301
|
-
|
|
323
|
+
if (init.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
324
|
+
init.operator === '||') {
|
|
302
325
|
// Check if right side is a non-boolean literal (array, object, string, number)
|
|
303
|
-
const rightSide =
|
|
326
|
+
const rightSide = init.right;
|
|
304
327
|
if (rightSide.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
305
328
|
rightSide.type === utils_1.AST_NODE_TYPES.ObjectExpression ||
|
|
306
329
|
(rightSide.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
@@ -314,7 +337,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
314
337
|
}
|
|
315
338
|
// For other cases, we need to be more careful
|
|
316
339
|
// If we can determine the left side is a boolean, then it's a boolean variable
|
|
317
|
-
const leftSide =
|
|
340
|
+
const leftSide = unwrapChainExpression(init.left);
|
|
318
341
|
if ((leftSide.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
319
342
|
typeof leftSide.value === 'boolean') ||
|
|
320
343
|
(leftSide.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
@@ -336,20 +359,20 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
336
359
|
return false;
|
|
337
360
|
}
|
|
338
361
|
// Check for unary expressions with ! operator
|
|
339
|
-
if (
|
|
340
|
-
|
|
362
|
+
if (init.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
363
|
+
init.operator === '!') {
|
|
341
364
|
return true;
|
|
342
365
|
}
|
|
343
366
|
// Check for function calls that might return boolean
|
|
344
|
-
if (
|
|
345
|
-
|
|
367
|
+
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
368
|
+
init.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
346
369
|
// A coercion through the global `Boolean` is as definitive as `!!x`,
|
|
347
370
|
// and its callee carries no approved prefix for the name heuristic
|
|
348
371
|
// below to recognize.
|
|
349
|
-
if (isGlobalBooleanCall(
|
|
372
|
+
if (isGlobalBooleanCall(init)) {
|
|
350
373
|
return true;
|
|
351
374
|
}
|
|
352
|
-
const calleeName =
|
|
375
|
+
const calleeName = init.callee.name;
|
|
353
376
|
const lowerCallee = calleeName.toLowerCase();
|
|
354
377
|
// For assert*-style utilities, only treat as boolean if we can confirm boolean return type
|
|
355
378
|
if (lowerCallee.startsWith('assert')) {
|
|
@@ -1110,7 +1133,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1110
1133
|
const variableDeclarator = node.parent;
|
|
1111
1134
|
if (variableDeclarator?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
1112
1135
|
variableDeclarator.init) {
|
|
1113
|
-
const init = variableDeclarator.init;
|
|
1136
|
+
const init = unwrapChainExpression(variableDeclarator.init);
|
|
1114
1137
|
// Check for direct boolean initialization
|
|
1115
1138
|
if (init.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1116
1139
|
typeof init.value === 'boolean') {
|
|
@@ -39,6 +39,22 @@ const referenceTypeNameOf = (typeName) => {
|
|
|
39
39
|
}
|
|
40
40
|
return undefined;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* The expression an optional link wraps, so a receiver spelled with `?.` is
|
|
44
|
+
* read as the expression it actually evaluates.
|
|
45
|
+
*
|
|
46
|
+
* `a?.b` interposes a `ChainExpression` between the member/call and its real
|
|
47
|
+
* parent. That link perturbs nullability, not the document schema:
|
|
48
|
+
* `db?.collection<T>('x')` has type `CollectionReference<T> | undefined`, whose
|
|
49
|
+
* schema is still `T`, never `DocumentData`. Leaving the wrapper in place makes
|
|
50
|
+
* a typed collection look unrecognizable, and the `.doc()` that inherits its
|
|
51
|
+
* schema draws a missing-generic report whose only remedy — `doc<T>(...)` —
|
|
52
|
+
* does not compile, since `CollectionReference<T>.doc` declares no type
|
|
53
|
+
* parameters.
|
|
54
|
+
*/
|
|
55
|
+
function unwrapOptionalChain(node) {
|
|
56
|
+
return node.type === utils_1.AST_NODE_TYPES.ChainExpression ? node.expression : node;
|
|
57
|
+
}
|
|
42
58
|
/**
|
|
43
59
|
* The type declaration a statement makes, looking through `export`.
|
|
44
60
|
*
|
|
@@ -344,9 +360,10 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
344
360
|
return false;
|
|
345
361
|
}
|
|
346
362
|
const isTypedCollectionReferenceCache = new Map();
|
|
347
|
-
function isTypedCollectionReference(
|
|
348
|
-
if (!
|
|
363
|
+
function isTypedCollectionReference(receiver) {
|
|
364
|
+
if (!receiver)
|
|
349
365
|
return false;
|
|
366
|
+
const node = unwrapOptionalChain(receiver);
|
|
350
367
|
if (isTypedCollectionReferenceCache.has(node)) {
|
|
351
368
|
return isTypedCollectionReferenceCache.get(node);
|
|
352
369
|
}
|
|
@@ -511,10 +528,11 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
511
528
|
}
|
|
512
529
|
return isTypedCollectionInitializer(declarator.init);
|
|
513
530
|
}
|
|
514
|
-
function isTypedCollectionInitializer(
|
|
515
|
-
if (!
|
|
531
|
+
function isTypedCollectionInitializer(initializer) {
|
|
532
|
+
if (!initializer) {
|
|
516
533
|
return false;
|
|
517
534
|
}
|
|
535
|
+
const init = unwrapOptionalChain(initializer);
|
|
518
536
|
// An explicit assertion states the schema just as an annotation does.
|
|
519
537
|
if (init.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
520
538
|
return hasCollectionReferenceType(init.typeAnnotation);
|
|
@@ -193,9 +193,37 @@ function isPrimitiveLiteral(node) {
|
|
|
193
193
|
typeof value === 'boolean' ||
|
|
194
194
|
typeof value === 'bigint');
|
|
195
195
|
}
|
|
196
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Strips the wrappers that leave an expression's shape intact, assertions plus
|
|
198
|
+
* the `ChainExpression` an optional link parks on the outermost node of a chain.
|
|
199
|
+
*
|
|
200
|
+
* The two are kept apart rather than merged into `unwrapAssertions` because a
|
|
201
|
+
* chain is not erased at runtime: `admin?.firestore()` evaluates to the handle
|
|
202
|
+
* or to `undefined`, so a caller reasoning about the *value* an expression
|
|
203
|
+
* produces — `isPrimitiveLiteral` — must keep seeing the chain. A caller
|
|
204
|
+
* reasoning about the *shape* it is written in, which is what the evidence scan
|
|
205
|
+
* asks, must look through it: the optional link decides whether the handle is
|
|
206
|
+
* produced, never which instance it is.
|
|
207
|
+
*/
|
|
208
|
+
function unwrapTransparent(node) {
|
|
209
|
+
const stripped = unwrapAssertions(node);
|
|
210
|
+
return stripped.type === utils_1.AST_NODE_TYPES.ChainExpression
|
|
211
|
+
? unwrapTransparent(stripped.expression)
|
|
212
|
+
: stripped;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Whether a declarator is initialized from a `<x>.firestore()` call.
|
|
216
|
+
*
|
|
217
|
+
* Both optional spellings — `admin?.firestore()` and `admin.firestore?.()` —
|
|
218
|
+
* parse as `ChainExpression > CallExpression`, so testing the initializer's own
|
|
219
|
+
* type read `ChainExpression` and answered no. Since this scan is the last
|
|
220
|
+
* detector left for a bare-identifier receiver, that miss dropped the report
|
|
221
|
+
* silently, and it hit the more careful spellings hardest: `admin.apps[0]?.
|
|
222
|
+
* firestore()` and `admin.app()?.firestore()` are the idiomatic admin-SDK
|
|
223
|
+
* singleton bootstrap, not exotic code.
|
|
224
|
+
*/
|
|
197
225
|
function initializesFirestore(declarator) {
|
|
198
|
-
const
|
|
226
|
+
const init = declarator.init ? unwrapTransparent(declarator.init) : null;
|
|
199
227
|
return (init?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
200
228
|
init.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
201
229
|
init.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -377,6 +377,16 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
377
377
|
* Check if a node represents a valid query key usage
|
|
378
378
|
*/
|
|
379
379
|
function isValidQueryKeyUsage(node) {
|
|
380
|
+
// `config?.getQueryKey()` parses as a `ChainExpression` wrapping the call,
|
|
381
|
+
// a type this switch does not name — so the optional spelling alone fell
|
|
382
|
+
// through to `return false` and bypassed the carve-outs below, reporting a
|
|
383
|
+
// key the plain spelling is allowed to build (#1832). Optionality is
|
|
384
|
+
// orthogonal to what this function asks: the question is where the key
|
|
385
|
+
// comes from, and a short-circuit changes only whether the same source is
|
|
386
|
+
// evaluated, never which source it is.
|
|
387
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
388
|
+
return isValidQueryKeyUsage(node.expression);
|
|
389
|
+
}
|
|
380
390
|
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
381
391
|
const importInfo = queryKeyImports.get(node.name);
|
|
382
392
|
if (importInfo && isQueryKeysSource(importInfo.source)) {
|
|
@@ -66,6 +66,17 @@ function bindsFastDeepEqual(variable) {
|
|
|
66
66
|
fastDeepEqualModules_1.FAST_DEEP_EQUAL_MODULES.has(String(declaration.source.value)));
|
|
67
67
|
}));
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The expression an optional chain wraps. ESTree interposes a
|
|
71
|
+
* `ChainExpression` between an optional member/call and its real parent, so
|
|
72
|
+
* `changes?.length` reaches an operand test as a ChainExpression while
|
|
73
|
+
* `changes.length` reaches it as a MemberExpression. Every arm that inspects an
|
|
74
|
+
* operand has to unwrap first, or one spelling of a single idiom escapes the
|
|
75
|
+
* rule while the other is reported.
|
|
76
|
+
*/
|
|
77
|
+
function unwrapChain(node) {
|
|
78
|
+
return node.type === utils_1.AST_NODE_TYPES.ChainExpression ? node.expression : node;
|
|
79
|
+
}
|
|
69
80
|
/**
|
|
70
81
|
* Whether two edits touch the same characters. ESLint sorts the fixes of one
|
|
71
82
|
* report and asserts each starts at or after the end of the previous one, so
|
|
@@ -107,7 +118,6 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
107
118
|
* violations still emit `isEqual(...)` calls, leaving them unbound.
|
|
108
119
|
*/
|
|
109
120
|
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
110
|
-
const isChainExpression = (node) => node.type === utils_1.AST_NODE_TYPES.ChainExpression;
|
|
111
121
|
function isMicrodiffCallee(callee) {
|
|
112
122
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
113
123
|
callee.name === microdiffImportName) {
|
|
@@ -265,11 +275,16 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
265
275
|
'!=',
|
|
266
276
|
];
|
|
267
277
|
if (operators.includes(node.operator)) {
|
|
278
|
+
// `diff(a, b)?.length`, `changes?.length` and `diff?.(a, b).length`
|
|
279
|
+
// each reach the operand as a ChainExpression, so the unwrap is what
|
|
280
|
+
// keeps the comparison spellings of one idiom from diverging.
|
|
281
|
+
const left = unwrapChain(node.left);
|
|
282
|
+
const right = unwrapChain(node.right);
|
|
268
283
|
// side A: MemberExpression .length, side B: 0
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const { diffCall } = getMicrodiffCallFromLengthAccess(
|
|
284
|
+
if (right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
285
|
+
right.value === 0 &&
|
|
286
|
+
left.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
287
|
+
const { diffCall } = getMicrodiffCallFromLengthAccess(left);
|
|
273
288
|
if (diffCall) {
|
|
274
289
|
return {
|
|
275
290
|
isEquality: node.operator === '===' || node.operator === '==',
|
|
@@ -278,10 +293,10 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
278
293
|
}
|
|
279
294
|
}
|
|
280
295
|
// side A: 0, side B: MemberExpression .length
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const { diffCall } = getMicrodiffCallFromLengthAccess(
|
|
296
|
+
if (left.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
297
|
+
left.value === 0 &&
|
|
298
|
+
right.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
299
|
+
const { diffCall } = getMicrodiffCallFromLengthAccess(right);
|
|
285
300
|
if (diffCall) {
|
|
286
301
|
return {
|
|
287
302
|
isEquality: node.operator === '===' || node.operator === '==',
|
|
@@ -294,10 +309,7 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
294
309
|
// Check for unary expressions like !diff(a, b).length or !changes.length (including optional chaining)
|
|
295
310
|
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
296
311
|
node.operator === '!') {
|
|
297
|
-
const
|
|
298
|
-
const target = isChainExpression(argumentNode)
|
|
299
|
-
? argumentNode.expression
|
|
300
|
-
: argumentNode;
|
|
312
|
+
const target = unwrapChain(node.argument);
|
|
301
313
|
if (target.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
302
314
|
const { diffCall } = getMicrodiffCallFromLengthAccess(target);
|
|
303
315
|
if (diffCall) {
|
|
@@ -315,13 +327,18 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
315
327
|
* Try to find the identifier used as `<id>.length` for the given equality node
|
|
316
328
|
*/
|
|
317
329
|
function getLengthIdentifierFromNode(node) {
|
|
330
|
+
// The operands are unwrapped for the same reason the detection arm
|
|
331
|
+
// unwraps them: `changes?.length` hides the identifier behind a
|
|
332
|
+
// ChainExpression. Missing it here does not silence the report — it
|
|
333
|
+
// rewrites the comparison and leaves the now-dead
|
|
334
|
+
// `const changes = diff(a, b);` behind.
|
|
335
|
+
const isLengthMember = (n) => n.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
336
|
+
!n.computed &&
|
|
337
|
+
n.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
338
|
+
n.property.name === 'length';
|
|
318
339
|
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
319
|
-
const left = node.left;
|
|
320
|
-
const right = node.right;
|
|
321
|
-
const isLengthMember = (n) => n.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
322
|
-
!n.computed &&
|
|
323
|
-
n.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
324
|
-
n.property.name === 'length';
|
|
340
|
+
const left = unwrapChain(node.left);
|
|
341
|
+
const right = unwrapChain(node.right);
|
|
325
342
|
if (isLengthMember(left) &&
|
|
326
343
|
left.object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
327
344
|
return left.object;
|
|
@@ -332,11 +349,8 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
332
349
|
}
|
|
333
350
|
}
|
|
334
351
|
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
335
|
-
const arg = node.argument;
|
|
336
|
-
if (arg
|
|
337
|
-
!arg.computed &&
|
|
338
|
-
arg.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
339
|
-
arg.property.name === 'length' &&
|
|
352
|
+
const arg = unwrapChain(node.argument);
|
|
353
|
+
if (isLengthMember(arg) &&
|
|
340
354
|
arg.object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
341
355
|
return arg.object;
|
|
342
356
|
}
|
|
@@ -4,5 +4,6 @@ type Options = [
|
|
|
4
4
|
functionPatterns?: string[];
|
|
5
5
|
}
|
|
6
6
|
];
|
|
7
|
-
|
|
7
|
+
type MessageIds = 'noDirectFunctionState' | 'noDirectFunctionStateAssertion';
|
|
8
|
+
export declare const noDirectFunctionState: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
|
|
8
9
|
export {};
|