@blumintinc/eslint-plugin-blumint 1.21.2 → 1.21.3
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-boolean-naming-prefixes.js +28 -5
- package/lib/rules/enforce-centralized-mock-firestore.d.ts +2 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +173 -38
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +78 -13
- package/lib/rules/enforce-firestore-set-merge.js +155 -4
- package/lib/rules/enforce-object-literal-as-const.js +85 -4
- package/lib/rules/enforce-positive-naming.js +17 -13
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +1 -1
- package/lib/rules/enforce-snapshot-state-narrowing.js +39 -19
- package/lib/rules/memo-nested-react-components.js +12 -0
- package/lib/rules/no-entire-object-hook-deps.js +26 -3
- package/lib/rules/no-hungarian.js +14 -0
- package/lib/rules/no-inline-component-prop.js +83 -14
- package/lib/rules/no-restricted-properties-fix.js +53 -3
- package/lib/rules/no-useless-usememo-primitives.js +43 -11
- package/lib/rules/prefer-destructuring-no-class.js +65 -11
- package/lib/rules/prefer-document-flattening.js +71 -35
- package/lib/rules/require-memoize-jsx-returners.js +18 -4
- package/lib/rules/use-custom-memo.js +4 -6
- package/lib/utils/memoModule.d.ts +26 -0
- package/lib/utils/memoModule.js +54 -0
- package/package.json +1 -1
- package/release-manifest.json +134 -0
|
@@ -33,6 +33,22 @@ const BATCH_MANAGER = 'batchManager';
|
|
|
33
33
|
* the rule's scope entirely.
|
|
34
34
|
*/
|
|
35
35
|
const REALTIME_BATCH_MANAGER = 'RealtimeBatchManager';
|
|
36
|
+
/** Firestore's single-field addressing class, in every spelling it reaches. */
|
|
37
|
+
const FIELD_PATH = 'FieldPath';
|
|
38
|
+
/**
|
|
39
|
+
* Receivers whose `update(…)` puts the document reference in the first
|
|
40
|
+
* position. See {@link takesReferenceFirst}.
|
|
41
|
+
*/
|
|
42
|
+
const REFERENCE_FIRST_RECEIVERS = new Set([
|
|
43
|
+
'batch',
|
|
44
|
+
'writebatch',
|
|
45
|
+
'bulkwriter',
|
|
46
|
+
'transaction',
|
|
47
|
+
'trx',
|
|
48
|
+
'tx',
|
|
49
|
+
'txn',
|
|
50
|
+
'writer',
|
|
51
|
+
]);
|
|
36
52
|
/** The firestore module a dynamic `await import(…)` reads, if it is one. */
|
|
37
53
|
function firestoreDynamicImportModule(node) {
|
|
38
54
|
if (node?.type !== utils_1.AST_NODE_TYPES.AwaitExpression) {
|
|
@@ -269,6 +285,88 @@ function unwrapTransparent(node) {
|
|
|
269
285
|
? unwrapTransparent(stripped.expression)
|
|
270
286
|
: stripped;
|
|
271
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Whether an expression names Firestore's `FieldPath`, which addresses a single
|
|
290
|
+
* field rather than carrying a document's data.
|
|
291
|
+
*
|
|
292
|
+
* Every spelling reaches the same class — a bare `new FieldPath('a', 'b')`, the
|
|
293
|
+
* namespaced `new admin.firestore.FieldPath(…)`, and the static
|
|
294
|
+
* `admin.firestore.FieldPath.documentId()` — so the rightmost `FieldPath`
|
|
295
|
+
* segment anywhere in the callee or the member chain is what answers.
|
|
296
|
+
*/
|
|
297
|
+
function namesFieldPath(node) {
|
|
298
|
+
const expression = unwrapTransparent(node);
|
|
299
|
+
switch (expression.type) {
|
|
300
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
301
|
+
return expression.name === FIELD_PATH;
|
|
302
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
303
|
+
return ((!expression.computed &&
|
|
304
|
+
expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
305
|
+
expression.property.name === FIELD_PATH) ||
|
|
306
|
+
namesFieldPath(expression.object));
|
|
307
|
+
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
308
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
309
|
+
return namesFieldPath(expression.callee);
|
|
310
|
+
default:
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Whether an argument is the document-data object the rewrite appends its
|
|
316
|
+
* options to, rather than the field name of `update(field, value, …)`.
|
|
317
|
+
*
|
|
318
|
+
* A primitive and a `FieldPath` are the two things the varargs overload puts in
|
|
319
|
+
* that position, and neither is data: handing one to `set` makes it the whole
|
|
320
|
+
* document (#2311).
|
|
321
|
+
*/
|
|
322
|
+
function isDocumentData(node) {
|
|
323
|
+
return !!node && !isPrimitiveLiteral(node) && !namesFieldPath(node);
|
|
324
|
+
}
|
|
325
|
+
/** The identifier that names a receiver, however the expression reaches it. */
|
|
326
|
+
function receiverName(node) {
|
|
327
|
+
const receiver = unwrapTransparent(node);
|
|
328
|
+
switch (receiver.type) {
|
|
329
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
330
|
+
return receiver.name;
|
|
331
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
332
|
+
return !receiver.computed &&
|
|
333
|
+
receiver.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
334
|
+
? receiver.property.name
|
|
335
|
+
: null;
|
|
336
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
337
|
+
return receiverName(receiver.callee);
|
|
338
|
+
default:
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** A name's last camelCase (or snake_case) segment, lowercased. */
|
|
343
|
+
function lastNameSegment(name) {
|
|
344
|
+
const segments = name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? [];
|
|
345
|
+
return (segments[segments.length - 1] ?? '').toLowerCase();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Whether a receiver's `update(…)` takes the document reference FIRST, which
|
|
349
|
+
* puts the data it merges in the second position.
|
|
350
|
+
*
|
|
351
|
+
* `WriteBatch`, `Transaction` and `BulkWriter` all spell the call
|
|
352
|
+
* `update(ref, data)`, while a `DocumentReference` spells it `update(data)`.
|
|
353
|
+
* The two readings of a two-argument call are not interchangeable — on a
|
|
354
|
+
* reference the second argument is a `Precondition`, which `set` has no
|
|
355
|
+
* parameter for — so the shape gate has to know which one it is looking at.
|
|
356
|
+
*
|
|
357
|
+
* The match is on the receiver's last camelCase SEGMENT rather than on a
|
|
358
|
+
* substring: a name that merely contains one of these words is usually a
|
|
359
|
+
* document, `transactionRef` being a document in a `transactions` collection.
|
|
360
|
+
* An unrecognised receiver therefore reads as a document, which is the safe
|
|
361
|
+
* default in both directions: its one-argument calls still rewrite, because
|
|
362
|
+
* `update(data)` is the only valid reading of a single argument on ANY of these
|
|
363
|
+
* receivers, and its two-argument calls decline rather than gamble the
|
|
364
|
+
* precondition rewrite on a name.
|
|
365
|
+
*/
|
|
366
|
+
function takesReferenceFirst(node) {
|
|
367
|
+
const name = receiverName(node);
|
|
368
|
+
return name !== null && REFERENCE_FIRST_RECEIVERS.has(lastNameSegment(name));
|
|
369
|
+
}
|
|
272
370
|
/**
|
|
273
371
|
* Whether a declarator is initialized from a `<x>.firestore()` call.
|
|
274
372
|
*
|
|
@@ -953,7 +1051,11 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
953
1051
|
// BatchManager takes a single descriptor object, so its arguments are
|
|
954
1052
|
// genuinely restructured rather than extended.
|
|
955
1053
|
if (objectText.includes('batchManager')) {
|
|
956
|
-
|
|
1054
|
+
// The descriptor is built from the reference and the data alone, so an
|
|
1055
|
+
// argument past the second is copied nowhere: a call carrying one is
|
|
1056
|
+
// some other overload, and rewriting it would DELETE that argument
|
|
1057
|
+
// rather than merely mislay it (#2311).
|
|
1058
|
+
if (args.length !== 2 || !isDocumentData(args[1])) {
|
|
957
1059
|
return null;
|
|
958
1060
|
}
|
|
959
1061
|
// This branch is the one rewrite that cannot edit the call in place: it
|
|
@@ -985,6 +1087,47 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
985
1087
|
`${indent}})`),
|
|
986
1088
|
];
|
|
987
1089
|
}
|
|
1090
|
+
// `set` has exactly two parameters, `set(data, options)`, so appending
|
|
1091
|
+
// `{ merge: true }` is a valid rewrite of ONE call shape: the single
|
|
1092
|
+
// data object. `update` has two more documented forms, and the append
|
|
1093
|
+
// corrupts both (#2311).
|
|
1094
|
+
//
|
|
1095
|
+
// `update(field, value, …)` addresses fields positionally. Appending
|
|
1096
|
+
// there emits `set(field, value, …, { merge: true })`, where `set` reads
|
|
1097
|
+
// the field NAME as the document data and every argument past the second
|
|
1098
|
+
// is a type error.
|
|
1099
|
+
//
|
|
1100
|
+
// `update(data, precondition)` guards the write on the document's
|
|
1101
|
+
// existence or its version. Appending there emits
|
|
1102
|
+
// `set(data, precondition, { merge: true })`: `set` reads the
|
|
1103
|
+
// precondition as its `SetOptions`, which carries neither `merge` nor
|
|
1104
|
+
// `mergeFields`, so the guard is dropped AND the merge never applies —
|
|
1105
|
+
// and a `set` without merge overwrites the whole document, deleting
|
|
1106
|
+
// every field the partial update was not naming. That rewrite exits 0
|
|
1107
|
+
// and produces no diagnostic without type information, which is why the
|
|
1108
|
+
// fix is withheld rather than approximated.
|
|
1109
|
+
//
|
|
1110
|
+
// The REPORT stands in both cases. Neither form has a mechanical `set`
|
|
1111
|
+
// equivalent — the varargs one has to be folded into an object literal
|
|
1112
|
+
// and the precondition has no `set` counterpart at all — so the author is
|
|
1113
|
+
// told, converts by hand, or opts out with a reviewable
|
|
1114
|
+
// `eslint-disable-next-line`. Suppressing the report instead would hand
|
|
1115
|
+
// the decision to a receiver-name heuristic: a receiver this rule fails
|
|
1116
|
+
// to recognise as a batch would stop reporting a violation it can see,
|
|
1117
|
+
// turning a declined FIX into an unenforced RULE.
|
|
1118
|
+
const dataIndex = takesReferenceFirst(callee.object) ? 1 : 0;
|
|
1119
|
+
if (args.length !== dataIndex + 1 || !isDocumentData(args[dataIndex])) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
// A receiver NAMED like a batch may still be a document — `const batch =
|
|
1123
|
+
// db.collection('batches').doc(id)` — and its two-argument call is then
|
|
1124
|
+
// the precondition overload after all. No batch or transaction passes a
|
|
1125
|
+
// document's data where its reference goes, so an object literal in the
|
|
1126
|
+
// first position withdraws the name's evidence and the call declines.
|
|
1127
|
+
if (dataIndex === 1 &&
|
|
1128
|
+
unwrapTransparent(args[0]).type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
988
1131
|
const appended = appendArguments(fixer, node, [MERGE_OPTION], 'set'.length - callee.property.name.length);
|
|
989
1132
|
if (!appended) {
|
|
990
1133
|
return null;
|
|
@@ -1164,11 +1307,19 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
1164
1307
|
* fix that never ships would make its own report decline too.
|
|
1165
1308
|
*/
|
|
1166
1309
|
function rewriteCall(fixer, rewrite) {
|
|
1310
|
+
// The modular SDK spells the varargs overload
|
|
1311
|
+
// `updateDoc(ref, field, value, …)`, which the append corrupts exactly as
|
|
1312
|
+
// it corrupts the method form: `setDoc(ref, field, value, …)` reads the
|
|
1313
|
+
// field NAME as the document data and overwrites the document with it
|
|
1314
|
+
// (#2311). `setDoc` has no third parameter past its options, so anything
|
|
1315
|
+
// beyond `updateDoc(ref, data)` declines.
|
|
1316
|
+
const args = rewrite.call.arguments;
|
|
1317
|
+
if (args.length > 2 || (args.length === 2 && !isDocumentData(args[1]))) {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1167
1320
|
// `setDoc` takes the document data between the reference and the
|
|
1168
1321
|
// options, so a call that passed no data gets an empty object to merge.
|
|
1169
|
-
const appended = appendArguments(fixer, rewrite.call,
|
|
1170
|
-
? [MERGE_OPTION]
|
|
1171
|
-
: [EMPTY_DATA, MERGE_OPTION], SET_DOC.length - rewrite.identifier.name.length);
|
|
1322
|
+
const appended = appendArguments(fixer, rewrite.call, args.length > 1 ? [MERGE_OPTION] : [EMPTY_DATA, MERGE_OPTION], SET_DOC.length - rewrite.identifier.name.length);
|
|
1172
1323
|
if (!appended) {
|
|
1173
1324
|
return null;
|
|
1174
1325
|
}
|
|
@@ -117,15 +117,93 @@ function declaredReturnTypeOf(fn) {
|
|
|
117
117
|
}
|
|
118
118
|
return undefined;
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Every `return` statement lexically owned by `fn` — a `return` inside a
|
|
122
|
+
* nested function returns from that function, not this one, so descent stops
|
|
123
|
+
* at each function boundary. Both single-node and array-valued child
|
|
124
|
+
* properties are walked (`BlockStatement.body` is an array,
|
|
125
|
+
* `IfStatement.consequent` a single node): a walk keyed on a plain `isNode`
|
|
126
|
+
* check alone silently skips every array-shaped child.
|
|
127
|
+
*/
|
|
128
|
+
function returnStatementsOf(fn) {
|
|
129
|
+
const returns = [];
|
|
130
|
+
function visit(node) {
|
|
131
|
+
if (node.type === 'ReturnStatement') {
|
|
132
|
+
returns.push(node);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (node !== fn && FUNCTION_TYPES.has(node.type)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const key in node) {
|
|
139
|
+
if (key === 'parent') {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const value = node[key];
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
for (const item of value) {
|
|
145
|
+
if (ASTHelpers_1.ASTHelpers.isNode(item)) {
|
|
146
|
+
visit(item);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
|
|
151
|
+
visit(value);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
visit(fn.body);
|
|
156
|
+
return returns;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The type carried by the sole `return` statement's own `as` assertion — a
|
|
160
|
+
* fallback reading used only when the signature itself states nothing. A
|
|
161
|
+
* single return whose expression is asserted to a type states that type as
|
|
162
|
+
* forcefully as a signature would: TS infers nothing beyond it, since there
|
|
163
|
+
* is nothing else to union it with. Two or more returns lose this — each
|
|
164
|
+
* return's own assertion describes only its branch, and the signature TS
|
|
165
|
+
* infers unions them, so no single assertion speaks for the whole function.
|
|
166
|
+
*
|
|
167
|
+
* Reading only the signature is exactly what let
|
|
168
|
+
* `no-redundant-annotation-assertion`'s `--fix` disarm this rule: deleting a
|
|
169
|
+
* signature that repeats a sole return's assertion moves the type
|
|
170
|
+
* information rather than removing it, and this rule read only the moved-from
|
|
171
|
+
* location (#2319).
|
|
172
|
+
*
|
|
173
|
+
* `as const` is excluded: it names no separate type to read here — it is the
|
|
174
|
+
* very rewrite this rule is deciding whether to apply, not a statement of
|
|
175
|
+
* what a signature would accept.
|
|
176
|
+
*/
|
|
177
|
+
function soleReturnAssertionTypeOf(fn) {
|
|
178
|
+
const returns = returnStatementsOf(fn);
|
|
179
|
+
if (returns.length !== 1) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const { argument } = returns[0];
|
|
183
|
+
if (!argument || argument.type !== 'TSAsExpression') {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const { typeAnnotation } = argument;
|
|
187
|
+
const isAsConst = typeAnnotation.type === 'TSTypeReference' &&
|
|
188
|
+
typeAnnotation.typeName.type === 'Identifier' &&
|
|
189
|
+
typeAnnotation.typeName.name === 'const';
|
|
190
|
+
return isAsConst ? undefined : typeAnnotation;
|
|
191
|
+
}
|
|
120
192
|
/**
|
|
121
193
|
* The type the *returned expression* must satisfy. For an async function or a
|
|
122
194
|
* generator the declared return type wraps that expression's type, so the
|
|
123
195
|
* wrapper is peeled off before the annotation is judged.
|
|
196
|
+
*
|
|
197
|
+
* With no signature annotation in view, the sole return statement's own
|
|
198
|
+
* assertion (`soleReturnAssertionTypeOf`) is read directly as the value's
|
|
199
|
+
* type instead: it targets the returned expression itself, not a
|
|
200
|
+
* function-level return type, so it carries no Promise/Generator wrapper to
|
|
201
|
+
* peel the way a signature annotation would.
|
|
124
202
|
*/
|
|
125
203
|
function returnedValueTypeOf(fn) {
|
|
126
204
|
const declared = declaredReturnTypeOf(fn);
|
|
127
205
|
if (!declared) {
|
|
128
|
-
return
|
|
206
|
+
return soleReturnAssertionTypeOf(fn);
|
|
129
207
|
}
|
|
130
208
|
const referenceName = typeReferenceNameOf(declared);
|
|
131
209
|
if (fn.generator) {
|
|
@@ -238,9 +316,12 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
|
238
316
|
return false;
|
|
239
317
|
}
|
|
240
318
|
const enclosingFunction = enclosingFunctionOf(ancestors);
|
|
241
|
-
// With no declared return type in view
|
|
242
|
-
//
|
|
243
|
-
|
|
319
|
+
// With no declared return type in view — neither the signature nor, as
|
|
320
|
+
// a fallback, the sole return statement's own assertion — the inferred
|
|
321
|
+
// tuple is what the callers get.
|
|
322
|
+
if (!enclosingFunction ||
|
|
323
|
+
(!declaredReturnTypeOf(enclosingFunction) &&
|
|
324
|
+
!soleReturnAssertionTypeOf(enclosingFunction))) {
|
|
244
325
|
return true;
|
|
245
326
|
}
|
|
246
327
|
const returnedValueType = returnedValueTypeOf(enclosingFunction);
|
|
@@ -903,6 +903,21 @@ function splitNameIntoWords(name) {
|
|
|
903
903
|
const spacedName = name.replace(/([A-Z])/g, ' $1');
|
|
904
904
|
return spacedName.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
905
905
|
}
|
|
906
|
+
// Name prefixes that mark a binding as a predicate rather than a value.
|
|
907
|
+
const BOOLEAN_NAME_PREFIXES = ['is', 'has', 'can', 'should', 'will', 'does'];
|
|
908
|
+
/**
|
|
909
|
+
* Whether a name reads as boolean-like, judged on the name's WORDS rather than
|
|
910
|
+
* its raw spelling. A casing-sensitive prefix test answers `hasNoAccess` and
|
|
911
|
+
* `HAS_NO_ACCESS` differently even though they carry the same meaning, and
|
|
912
|
+
* `global-const-style` mandates the SCREAMING_SNAKE spelling for module-level
|
|
913
|
+
* constants — so every module constant would sit outside this rule's reach.
|
|
914
|
+
* Joining the split words normalizes camelCase, PascalCase, SCREAMING_SNAKE and
|
|
915
|
+
* snake_case onto one lowercase form before the prefix test runs.
|
|
916
|
+
*/
|
|
917
|
+
function hasBooleanNamePrefix(name) {
|
|
918
|
+
const normalizedName = splitNameIntoWords(name).join('');
|
|
919
|
+
return BOOLEAN_NAME_PREFIXES.some((prefix) => normalizedName.startsWith(prefix));
|
|
920
|
+
}
|
|
906
921
|
// Map of negative boolean terms to suggested positive alternatives
|
|
907
922
|
const BOOLEAN_POSITIVE_ALTERNATIVES = {
|
|
908
923
|
// Boolean prefixes - These will be removed from suggestions
|
|
@@ -1248,14 +1263,8 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1248
1263
|
return { isNegative: false, alternatives: [] };
|
|
1249
1264
|
}
|
|
1250
1265
|
}
|
|
1251
|
-
const nameLowercase = name.toLowerCase();
|
|
1252
1266
|
// Check for negative prefixes in boolean-like variables
|
|
1253
|
-
if (
|
|
1254
|
-
nameLowercase.startsWith('has') ||
|
|
1255
|
-
nameLowercase.startsWith('can') ||
|
|
1256
|
-
nameLowercase.startsWith('should') ||
|
|
1257
|
-
nameLowercase.startsWith('will') ||
|
|
1258
|
-
nameLowercase.startsWith('does')) {
|
|
1267
|
+
if (hasBooleanNamePrefix(name)) {
|
|
1259
1268
|
// We already checked exception words above, so no need to check again
|
|
1260
1269
|
for (const prefix of BOOLEAN_NEGATIVE_PREFIXES) {
|
|
1261
1270
|
const prefixCapitalized = prefix.charAt(0).toUpperCase() + prefix.slice(1);
|
|
@@ -1405,12 +1414,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1405
1414
|
}
|
|
1406
1415
|
// Check if the node has a name that suggests it's a boolean
|
|
1407
1416
|
if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1408
|
-
(node.name
|
|
1409
|
-
node.name.startsWith('has') ||
|
|
1410
|
-
node.name.startsWith('can') ||
|
|
1411
|
-
node.name.startsWith('should') ||
|
|
1412
|
-
node.name.startsWith('will') ||
|
|
1413
|
-
node.name.startsWith('does'))) {
|
|
1417
|
+
hasBooleanNamePrefix(node.name)) {
|
|
1414
1418
|
return true;
|
|
1415
1419
|
}
|
|
1416
1420
|
return false;
|
|
@@ -82,6 +82,10 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
82
82
|
// renames it is not told to call a function their config says does not
|
|
83
83
|
// exist. Every report and suggestion supplies it.
|
|
84
84
|
noFalsyCheck: "Do not use boolean coercion on FirestoreSnapshotState<T>. All string states ('idle', 'loading', 'not-found') are truthy, so '{{expression}}' does not behave as intended. Use {{guard}}(state) to narrow to T, or compare explicitly (e.g., state === 'loading').",
|
|
85
|
+
// `??` is a distinct mistake from `||`, so it gets its own wording: the
|
|
86
|
+
// problem is not truthiness but that no state is nullish, which makes the
|
|
87
|
+
// fallback unreachable rather than merely unreliable.
|
|
88
|
+
noNullishFallback: "Do not use nullish coalescing on FirestoreSnapshotState<T>. No state ('idle', 'loading', 'not-found') is null or undefined, so '{{expression}}' always evaluates to the state itself and the fallback is unreachable, leaving a state string bound where data was expected. Use {{guard}}(state) to choose between them (e.g., {{guard}}(state) ? state : fallback), or compare explicitly (e.g., state === 'loading').",
|
|
85
89
|
noRawTypeof: "Do not use '{{expression}}' to narrow FirestoreSnapshotState<T> to data. Use {{guard}}(state) instead to maintain the abstraction boundary.",
|
|
86
90
|
},
|
|
87
91
|
},
|
|
@@ -342,21 +346,28 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
342
346
|
}
|
|
343
347
|
}
|
|
344
348
|
/**
|
|
345
|
-
* Reports a
|
|
349
|
+
* Reports a narrowing violation on a snapshot-state identifier and attaches
|
|
350
|
+
* the guard-based rewrite as its suggestion.
|
|
346
351
|
*
|
|
347
352
|
* `replacement` must match the polarity of the flagged expression: a falsy
|
|
348
353
|
* check reads `!isSnapshotReady(state)`, a truthy one `isSnapshotReady(state)`.
|
|
349
354
|
* `fixNode` defaults to the reported node but can be widened when the whole
|
|
350
355
|
* surrounding expression has to be rewritten (e.g. `state || fallback`).
|
|
351
356
|
*/
|
|
352
|
-
function
|
|
357
|
+
function reportNarrowing(messageId, node, expression, replacement, fixNode = node) {
|
|
353
358
|
context.report({
|
|
354
359
|
node,
|
|
355
|
-
messageId
|
|
360
|
+
messageId,
|
|
356
361
|
data: { expression, guard: guardName },
|
|
357
|
-
suggest: guardSuggestion(
|
|
362
|
+
suggest: guardSuggestion(messageId, fixNode, replacement, context.getScope()),
|
|
358
363
|
});
|
|
359
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Reports a falsy/truthy check on a snapshot-state identifier.
|
|
367
|
+
*/
|
|
368
|
+
function reportFalsyCheck(node, expression, replacement, fixNode = node) {
|
|
369
|
+
reportNarrowing('noFalsyCheck', node, expression, replacement, fixNode);
|
|
370
|
+
}
|
|
360
371
|
return {
|
|
361
372
|
// Track variable declarations that come from snapshot hooks.
|
|
362
373
|
// Supports:
|
|
@@ -425,24 +436,33 @@ exports.enforceSnapshotStateNarrowing = (0, createRule_1.createRule)({
|
|
|
425
436
|
reportFalsyCheck(test, test.name, `${guardName}(${test.name})`);
|
|
426
437
|
}
|
|
427
438
|
},
|
|
428
|
-
// LogicalExpression: state && expr, state || expr
|
|
439
|
+
// LogicalExpression: state && expr, state || expr, state ?? expr
|
|
429
440
|
LogicalExpression(node) {
|
|
430
441
|
const left = node.left;
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
// `state || fallback` evaluates to the state itself when it is
|
|
441
|
-
// usable, so a bare operand swap would yield `true` instead of the
|
|
442
|
-
// data. Only the conditional form preserves that value.
|
|
443
|
-
const guarded = `${guardName}(${left.name}) ? ${left.name} : ${getText(node.right)}`;
|
|
444
|
-
reportFalsyCheck(left, left.name, needsParentheses(node) ? `(${guarded})` : guarded, node);
|
|
442
|
+
if (left.type !== utils_1.AST_NODE_TYPES.Identifier || !isSnapshotVar(left)) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (node.operator === '&&') {
|
|
446
|
+
// `state && expr` guards expr, so swapping the operand for the
|
|
447
|
+
// guard keeps both the polarity and the narrowing of `state`.
|
|
448
|
+
reportFalsyCheck(left, left.name, `${guardName}(${left.name})`);
|
|
449
|
+
return;
|
|
445
450
|
}
|
|
451
|
+
// `??` joins the `||` arm rather than the `&&` one: both are fallback
|
|
452
|
+
// forms whose left operand carries the value, so both need the same
|
|
453
|
+
// conditional rewrite. `??` is no safer than `||` on a snapshot state —
|
|
454
|
+
// every non-data member of the union is a truthy string and none is
|
|
455
|
+
// nullish, so neither operator can reach its fallback — but it fails
|
|
456
|
+
// for a different reason, so it reports under its own message.
|
|
457
|
+
const isNullishFallback = node.operator === '??';
|
|
458
|
+
// `state || fallback` evaluates to the state itself when it is
|
|
459
|
+
// usable, so a bare operand swap would yield `true` instead of the
|
|
460
|
+
// data. Only the conditional form preserves that value.
|
|
461
|
+
const guarded = `${guardName}(${left.name}) ? ${left.name} : ${getText(node.right)}`;
|
|
462
|
+
reportNarrowing(isNullishFallback ? 'noNullishFallback' : 'noFalsyCheck', left,
|
|
463
|
+
// The nullish message turns on the whole expression being dead, so it
|
|
464
|
+
// shows the operator and the fallback it can never reach.
|
|
465
|
+
isNullishFallback ? getText(node) : left.name, needsParentheses(node) ? `(${guarded})` : guarded, node);
|
|
446
466
|
},
|
|
447
467
|
// BinaryExpression: typeof state === 'object', typeof state !== 'string'
|
|
448
468
|
BinaryExpression(node) {
|
|
@@ -4,9 +4,21 @@ exports.memoNestedReactComponents = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const minimatch_1 = require("minimatch");
|
|
6
6
|
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
/**
|
|
8
|
+
* Memoization hooks whose first argument this rule inspects for a nested
|
|
9
|
+
* component.
|
|
10
|
+
*
|
|
11
|
+
* `useLatestCallback` belongs here because the sibling `use-latest-callback`
|
|
12
|
+
* rule rewrites `useCallback(fn, [])` into `useLatestCallback(fn)` under
|
|
13
|
+
* `--fix`. That rewrite changes only which hook wraps the callback; the
|
|
14
|
+
* component is still constructed inline inside render scope, which is the
|
|
15
|
+
* identity churn this rule reports. Omitting the name let the sibling's fix
|
|
16
|
+
* silently disarm this rule on the very code it had just flagged (#2313).
|
|
17
|
+
*/
|
|
7
18
|
const CALLBACK_HOOKS = new Set([
|
|
8
19
|
'useCallback',
|
|
9
20
|
'useDeepCompareCallback',
|
|
21
|
+
'useLatestCallback',
|
|
10
22
|
'useMemo',
|
|
11
23
|
'useDeepCompareMemo',
|
|
12
24
|
]);
|
|
@@ -4,13 +4,33 @@ exports.noEntireObjectHookDeps = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const typescript_1 = require("typescript");
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The hook calls whose last argument is a dependency array.
|
|
9
|
+
*
|
|
10
|
+
* why: the deep-compare family mirrors React's hooks argument-for-argument —
|
|
11
|
+
* callback first, dependency array last — so every analysis below applies to
|
|
12
|
+
* them unchanged. They are enumerated rather than matched by prefix because two
|
|
13
|
+
* fixable rules in this plugin (`prefer-use-deep-compare-memo` and
|
|
14
|
+
* `no-useless-usememo-primitives`) rewrite `useMemo` into `useDeepCompareMemo`
|
|
15
|
+
* while leaving the dependency array byte-identical; without these entries that
|
|
16
|
+
* rename would take the call out of this rule's sight. Deep comparison makes an
|
|
17
|
+
* entire-object dependency strictly WORSE, not acceptable: every render walks
|
|
18
|
+
* every property of the object rather than comparing one reference (#2309).
|
|
19
|
+
*/
|
|
20
|
+
const HOOK_NAMES = new Set([
|
|
21
|
+
'useEffect',
|
|
22
|
+
'useCallback',
|
|
23
|
+
'useMemo',
|
|
24
|
+
'useDeepCompareEffect',
|
|
25
|
+
'useDeepCompareCallback',
|
|
26
|
+
'useDeepCompareMemo',
|
|
27
|
+
]);
|
|
8
28
|
/**
|
|
9
29
|
* Hooks that run for their side effects rather than producing a value. An
|
|
10
30
|
* unread dependency means something different here than in useMemo/useCallback
|
|
11
31
|
* — see `callsCorrespondingSetter`.
|
|
12
32
|
*/
|
|
13
|
-
const EFFECT_HOOK_NAMES = new Set(['useEffect']);
|
|
33
|
+
const EFFECT_HOOK_NAMES = new Set(['useEffect', 'useDeepCompareEffect']);
|
|
14
34
|
/**
|
|
15
35
|
* Hooks that do not run their callback: they hand it back as a value, so the
|
|
16
36
|
* body executes only if — and when — the consumer invokes it.
|
|
@@ -20,7 +40,10 @@ const EFFECT_HOOK_NAMES = new Set(['useEffect']);
|
|
|
20
40
|
* body dereferences is therefore not licensed to appear in the array; see
|
|
21
41
|
* `collectGuardedPaths`.
|
|
22
42
|
*/
|
|
23
|
-
const DEFERRED_BODY_HOOK_NAMES = new Set([
|
|
43
|
+
const DEFERRED_BODY_HOOK_NAMES = new Set([
|
|
44
|
+
'useCallback',
|
|
45
|
+
'useDeepCompareCallback',
|
|
46
|
+
]);
|
|
24
47
|
/**
|
|
25
48
|
* A string key quoted the way a formatter quotes it.
|
|
26
49
|
*
|
|
@@ -757,6 +757,20 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
757
757
|
}
|
|
758
758
|
const parts = variableName.split('_');
|
|
759
759
|
const lastIndex = parts.length - 1;
|
|
760
|
+
// A leading single-letter segment (B_, I_) carries the same tag as the
|
|
761
|
+
// camelCase prefix (bIsActive), so global-const-style's rename to
|
|
762
|
+
// SCREAMING_SNAKE_CASE (bIsActive -> B_IS_ACTIVE) must not disarm the
|
|
763
|
+
// rule. Gated on `parts.length > 1` (guaranteed here, since the
|
|
764
|
+
// no-underscore case already returned above) so a lone `B` — with no
|
|
765
|
+
// following segment — is left alone: there is no tagged name to catch,
|
|
766
|
+
// only an isolated identifier. Reuses SINGLE_LETTER_PREFIXES rather than
|
|
767
|
+
// hardcoding b/i so the two casings can never diverge on which letters
|
|
768
|
+
// count. Real first-word segments (TAB_INDEX, LIB_VERSION, UI_CONFIG)
|
|
769
|
+
// are untouched because they are longer than one letter.
|
|
770
|
+
if (parts.length > 1 &&
|
|
771
|
+
SINGLE_LETTER_PREFIXES.has(parts[0].toLowerCase())) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
760
774
|
return TYPE_MARKERS.some((marker) => {
|
|
761
775
|
const markerUpper = marker.toUpperCase();
|
|
762
776
|
const normalizedMarker = marker.toLowerCase();
|