@blumintinc/eslint-plugin-blumint 1.21.2 → 1.21.4

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.
@@ -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
- if (args.length < 2) {
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, rewrite.call.arguments.length > 1
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 undefined;
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, the inferred tuple is what the
242
- // callers get.
243
- if (!enclosingFunction || !declaredReturnTypeOf(enclosingFunction)) {
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 (nameLowercase.startsWith('is') ||
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.startsWith('is') ||
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;
@@ -1,5 +1,5 @@
1
1
  import { TSESLint } from '@typescript-eslint/utils';
2
- type MessageIds = 'noFalsyCheck' | 'noRawTypeof';
2
+ type MessageIds = 'noFalsyCheck' | 'noNullishFallback' | 'noRawTypeof';
3
3
  type Options = [
4
4
  {
5
5
  snapshotHooks?: string[];
@@ -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 falsy/truthy check on a snapshot-state identifier.
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 reportFalsyCheck(node, expression, replacement, fixNode = node) {
357
+ function reportNarrowing(messageId, node, expression, replacement, fixNode = node) {
353
358
  context.report({
354
359
  node,
355
- messageId: 'noFalsyCheck',
360
+ messageId,
356
361
  data: { expression, guard: guardName },
357
- suggest: guardSuggestion('noFalsyCheck', fixNode, replacement, context.getScope()),
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 ((node.operator === '&&' || node.operator === '||') &&
432
- left.type === utils_1.AST_NODE_TYPES.Identifier &&
433
- isSnapshotVar(left)) {
434
- if (node.operator === '&&') {
435
- // `state && expr` guards expr, so swapping the operand for the
436
- // guard keeps both the polarity and the narrowing of `state`.
437
- reportFalsyCheck(left, left.name, `${guardName}(${left.name})`);
438
- return;
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
- const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
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(['useCallback']);
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();