@blumintinc/eslint-plugin-blumint 1.20.0 → 1.20.2

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.
@@ -265,14 +265,95 @@ function getFunctionName(fn) {
265
265
  }
266
266
  return findNameInAncestors(parent);
267
267
  }
268
+ /**
269
+ * Locates the ancestor that supplies a function's name, mirroring the walk
270
+ * `getFunctionName` performs through `findNameInAncestors` but yielding the
271
+ * NODE instead of the string, so callers can tell an object-property key apart
272
+ * from a variable or assignment binding.
273
+ */
274
+ function findNamingNode(fn) {
275
+ let current = fn.parent;
276
+ while (current) {
277
+ if (getNameFromNode(current)) {
278
+ return current;
279
+ }
280
+ if (!isTransparentNode(current)) {
281
+ return null;
282
+ }
283
+ current = current.parent;
284
+ }
285
+ return null;
286
+ }
287
+ /**
288
+ * True when a function expression is passed directly as an argument to a call
289
+ * (after unwrapping assertion/parenthesis wrappers). Function declarations are
290
+ * excluded because a declaration is a statement, never an argument.
291
+ */
292
+ function isCallbackArgument(fn) {
293
+ if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
294
+ return false;
295
+ }
296
+ let parent = fn.parent;
297
+ while (parent && isExpressionWrapper(parent)) {
298
+ parent = parent.parent;
299
+ }
300
+ return (parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
301
+ parent.arguments.some((arg) => unwrapNestedExpressions(arg) === fn));
302
+ }
303
+ /**
304
+ * True when a function's only claim to component/hook status is an object
305
+ * property key, and that object is assembled inside an anonymous callback which
306
+ * is itself neither a component nor a hook — the shape of a module factory
307
+ * (`jest.mock('m', () => ({ useThing: … }))`,
308
+ * `registerModule('m', () => ({ useThing: … }))`). Such a factory builds a
309
+ * value; React never renders it, so a `use*`/PascalCase key inside it names a
310
+ * member of that value rather than a render body. This is the same reasoning
311
+ * that makes `isComponentName` reject SCREAMING_SNAKE_CASE and that makes
312
+ * `isIterationMethodCallback` exempt discarded callbacks: a name match in a
313
+ * non-render context is not a render body.
314
+ *
315
+ * Requiring the enclosing function to be an anonymous CALL ARGUMENT is the
316
+ * deliberate narrowing that preserves genuine hook factories
317
+ * (`export function createApi(client) { return { useUser: … }; }`), whose
318
+ * returned hook really is consumed by React and really does need referential
319
+ * stability.
320
+ */
321
+ function isFactoryObjectMember(fn) {
322
+ const namingNode = findNamingNode(fn);
323
+ if (!namingNode || namingNode.type !== utils_1.AST_NODE_TYPES.Property) {
324
+ return false;
325
+ }
326
+ const objectLiteral = namingNode.parent;
327
+ if (!objectLiteral ||
328
+ objectLiteral.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
329
+ return false;
330
+ }
331
+ let current = objectLiteral.parent;
332
+ while (current) {
333
+ if (isFunctionNode(current)) {
334
+ return isCallbackArgument(current) && !isComponentOrHookFunction(current);
335
+ }
336
+ current = current.parent;
337
+ }
338
+ return false;
339
+ }
268
340
  /**
269
341
  * Checks whether a function should be treated as a React component or hook
270
342
  * based on naming conventions. Enables the rule to limit reports to user-facing
271
343
  * components and hooks rather than arbitrary functions.
344
+ *
345
+ * The name match is disqualified for members of a factory-built object (see
346
+ * `isFactoryObjectMember`). Disqualifying at the classifier — rather than
347
+ * skipping the report — keeps `findEnclosingComponentOrHook` walking outward,
348
+ * so a literal inside a factory nested in a real component is still attributed
349
+ * to that component.
272
350
  */
273
351
  function isComponentOrHookFunction(fn) {
274
352
  const name = getFunctionName(fn);
275
- return isComponentName(name) || isHookName(name);
353
+ if (!isComponentName(name) && !isHookName(name)) {
354
+ return false;
355
+ }
356
+ return !isFactoryObjectMember(fn);
276
357
  }
277
358
  /**
278
359
  * Checks whether a function node represents a hook by name.
@@ -329,6 +410,54 @@ function getLiteralDescriptor(node) {
329
410
  const descriptor = LITERAL_DESCRIPTOR_BY_TYPE[node.type] ?? null;
330
411
  return descriptor;
331
412
  }
413
+ /**
414
+ * Jest APIs whose second argument is a module factory: a function executed to
415
+ * BUILD a replacement module, hoisted above imports by `babel-plugin-jest-hoist`.
416
+ */
417
+ const JEST_MOCK_FACTORY_METHODS = new Set(['mock', 'doMock', 'setMock']);
418
+ /**
419
+ * True when `node` sits anywhere inside a jest module factory
420
+ * (`jest.mock('m', () => ({ … }))`). A factory is executed to build a
421
+ * replacement module, never rendered by React, so the rule's "on each render"
422
+ * premise does not hold for the test doubles it defines.
423
+ *
424
+ * Beyond the premise, neither remediation the rule advises is legal here:
425
+ * `babel-plugin-jest-hoist` rejects every out-of-scope reference inside a
426
+ * factory except `mock`-prefixed names, so `useMemo` cannot be imported or
427
+ * called, and hoisting to a module constant changes behaviour because the
428
+ * double must rebuild per call for tests to vary its result. This mirrors
429
+ * `isInsideIterationMethodCallback`, which exempts literals whose remediation
430
+ * is unfollowable at the literal.
431
+ *
432
+ * The walk is lexical, so a hook declared outside the factory in the same file
433
+ * stays analyzed.
434
+ */
435
+ function isInsideJestMockFactory(node) {
436
+ let current = node;
437
+ while (current) {
438
+ if (isFunctionNode(current) &&
439
+ current.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
440
+ let parent = current.parent;
441
+ while (parent && isExpressionWrapper(parent)) {
442
+ parent = parent.parent;
443
+ }
444
+ if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
445
+ parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current)) {
446
+ const { callee } = parent;
447
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
448
+ !callee.computed &&
449
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
450
+ callee.object.name === 'jest' &&
451
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
452
+ JEST_MOCK_FACTORY_METHODS.has(callee.property.name)) {
453
+ return true;
454
+ }
455
+ }
456
+ }
457
+ current = current.parent;
458
+ }
459
+ return false;
460
+ }
332
461
  /**
333
462
  * Finds the nearest ancestor function considered a React component or hook.
334
463
  * @param node Starting node for the search.
@@ -862,6 +991,329 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
862
991
  }
863
992
  return usages.every((ref) => isStyleJSXAttributeValue(ref.identifier));
864
993
  }
994
+ const PRIMITIVE_TYPE_KEYWORDS = new Set([
995
+ utils_1.AST_NODE_TYPES.TSBooleanKeyword,
996
+ utils_1.AST_NODE_TYPES.TSStringKeyword,
997
+ utils_1.AST_NODE_TYPES.TSNumberKeyword,
998
+ utils_1.AST_NODE_TYPES.TSBigIntKeyword,
999
+ utils_1.AST_NODE_TYPES.TSSymbolKeyword,
1000
+ utils_1.AST_NODE_TYPES.TSVoidKeyword,
1001
+ utils_1.AST_NODE_TYPES.TSNullKeyword,
1002
+ utils_1.AST_NODE_TYPES.TSUndefinedKeyword,
1003
+ utils_1.AST_NODE_TYPES.TSNeverKeyword,
1004
+ utils_1.AST_NODE_TYPES.TSTypePredicate,
1005
+ utils_1.AST_NODE_TYPES.TSTemplateLiteralType,
1006
+ ]);
1007
+ const NON_PRIMITIVE_EXPRESSION_TYPES = new Set([
1008
+ utils_1.AST_NODE_TYPES.ObjectExpression,
1009
+ utils_1.AST_NODE_TYPES.ArrayExpression,
1010
+ utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
1011
+ utils_1.AST_NODE_TYPES.FunctionExpression,
1012
+ utils_1.AST_NODE_TYPES.ClassExpression,
1013
+ utils_1.AST_NODE_TYPES.NewExpression,
1014
+ utils_1.AST_NODE_TYPES.JSXElement,
1015
+ utils_1.AST_NODE_TYPES.JSXFragment,
1016
+ ]);
1017
+ const COMPARISONISH_BINARY_OPERATORS = new Set([
1018
+ '===',
1019
+ '!==',
1020
+ '==',
1021
+ '!=',
1022
+ '<',
1023
+ '>',
1024
+ '<=',
1025
+ '>=',
1026
+ 'in',
1027
+ 'instanceof',
1028
+ ]);
1029
+ /**
1030
+ * Innermost scope containing `node`. Resolved from the source code rather
1031
+ * than `context.getScope()` because the latter is deprecated in ESLint 9
1032
+ * and reports the traversal position instead of the node's own scope.
1033
+ */
1034
+ function scopeOf(node) {
1035
+ const sourceCode = context.getSourceCode();
1036
+ return sourceCode.getScope ? sourceCode.getScope(node) : null;
1037
+ }
1038
+ function findVariableInScopes(name, startScope) {
1039
+ let currentScope = startScope;
1040
+ while (currentScope) {
1041
+ const variable = currentScope.variables.find((v) => v.name === name);
1042
+ if (variable)
1043
+ return variable;
1044
+ currentScope = currentScope.upper;
1045
+ }
1046
+ return undefined;
1047
+ }
1048
+ function isConstAssertion(typeNode) {
1049
+ return (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
1050
+ typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
1051
+ typeNode.typeName.name === 'const');
1052
+ }
1053
+ function classifyTypeAnnotation(typeNode) {
1054
+ if (PRIMITIVE_TYPE_KEYWORDS.has(typeNode.type)) {
1055
+ return 'primitive';
1056
+ }
1057
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSLiteralType) {
1058
+ return 'primitive';
1059
+ }
1060
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
1061
+ const members = typeNode.types.map(classifyTypeAnnotation);
1062
+ if (members.every((m) => m === 'primitive'))
1063
+ return 'primitive';
1064
+ return members.some((m) => m === 'nonPrimitive')
1065
+ ? 'nonPrimitive'
1066
+ : 'indeterminate';
1067
+ }
1068
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral ||
1069
+ typeNode.type === utils_1.AST_NODE_TYPES.TSArrayType ||
1070
+ typeNode.type === utils_1.AST_NODE_TYPES.TSTupleType ||
1071
+ typeNode.type === utils_1.AST_NODE_TYPES.TSFunctionType ||
1072
+ typeNode.type === utils_1.AST_NODE_TYPES.TSConstructorType ||
1073
+ typeNode.type === utils_1.AST_NODE_TYPES.TSObjectKeyword ||
1074
+ typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType ||
1075
+ typeNode.type === utils_1.AST_NODE_TYPES.TSMappedType ||
1076
+ typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
1077
+ return 'nonPrimitive';
1078
+ }
1079
+ return 'indeterminate';
1080
+ }
1081
+ function combineReturnEvaluations(entries) {
1082
+ if (entries.length === 0)
1083
+ return 'indeterminate';
1084
+ if (entries.every((e) => e === 'primitive'))
1085
+ return 'primitive';
1086
+ return entries.some((e) => e === 'nonPrimitive')
1087
+ ? 'nonPrimitive'
1088
+ : 'indeterminate';
1089
+ }
1090
+ function classifyReturnExpression(expression) {
1091
+ if (!expression)
1092
+ return 'primitive';
1093
+ if (expression.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
1094
+ expression.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
1095
+ expression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
1096
+ const annotation = expression.typeAnnotation;
1097
+ if (annotation && !isConstAssertion(annotation)) {
1098
+ const classified = classifyTypeAnnotation(annotation);
1099
+ if (classified !== 'indeterminate')
1100
+ return classified;
1101
+ }
1102
+ return classifyReturnExpression(expression.expression);
1103
+ }
1104
+ if (expression.type === utils_1.AST_NODE_TYPES.ChainExpression ||
1105
+ expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
1106
+ return classifyReturnExpression(expression.expression);
1107
+ }
1108
+ if (expression.type === PARENTHESIZED_EXPRESSION_TYPE) {
1109
+ return classifyReturnExpression(expression
1110
+ .expression);
1111
+ }
1112
+ if (expression.type === utils_1.AST_NODE_TYPES.SequenceExpression) {
1113
+ return classifyReturnExpression(expression.expressions[expression.expressions.length - 1]);
1114
+ }
1115
+ if (NON_PRIMITIVE_EXPRESSION_TYPES.has(expression.type)) {
1116
+ return 'nonPrimitive';
1117
+ }
1118
+ // Literals, template literals and update expressions are primitives by
1119
+ // construction.
1120
+ if (expression.type === utils_1.AST_NODE_TYPES.Literal ||
1121
+ expression.type === utils_1.AST_NODE_TYPES.TemplateLiteral ||
1122
+ expression.type === utils_1.AST_NODE_TYPES.UpdateExpression) {
1123
+ return 'primitive';
1124
+ }
1125
+ if (expression.type === utils_1.AST_NODE_TYPES.Identifier &&
1126
+ expression.name === 'undefined') {
1127
+ return 'primitive';
1128
+ }
1129
+ if (expression.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
1130
+ // `!`, `typeof`, `void`, `-`, `+`, `~`, `delete` all yield primitives.
1131
+ return 'primitive';
1132
+ }
1133
+ if (expression.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
1134
+ return COMPARISONISH_BINARY_OPERATORS.has(expression.operator)
1135
+ ? 'primitive'
1136
+ : 'indeterminate';
1137
+ }
1138
+ if (expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
1139
+ return combineReturnEvaluations([
1140
+ classifyReturnExpression(expression.consequent),
1141
+ classifyReturnExpression(expression.alternate),
1142
+ ]);
1143
+ }
1144
+ if (expression.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
1145
+ return combineReturnEvaluations([
1146
+ classifyReturnExpression(expression.left),
1147
+ classifyReturnExpression(expression.right),
1148
+ ]);
1149
+ }
1150
+ return 'indeterminate';
1151
+ }
1152
+ function collectReturnArgumentsOf(body) {
1153
+ const found = [];
1154
+ const walk = (node) => {
1155
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
1156
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
1157
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
1158
+ return;
1159
+ }
1160
+ if (node.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
1161
+ found.push(node.argument ?? null);
1162
+ return;
1163
+ }
1164
+ for (const key of Object.keys(node)) {
1165
+ if (key === 'parent')
1166
+ continue;
1167
+ const value = node[key];
1168
+ if (Array.isArray(value)) {
1169
+ for (const entry of value) {
1170
+ if (ASTHelpers_1.ASTHelpers.isNode(entry))
1171
+ walk(entry);
1172
+ }
1173
+ }
1174
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
1175
+ walk(value);
1176
+ }
1177
+ }
1178
+ };
1179
+ for (const statement of body.body) {
1180
+ walk(statement);
1181
+ }
1182
+ return found;
1183
+ }
1184
+ function classifyFunctionReturn(functionLike) {
1185
+ const annotation = functionLike.returnType?.typeAnnotation;
1186
+ if (annotation) {
1187
+ const classified = classifyTypeAnnotation(annotation);
1188
+ if (classified !== 'indeterminate')
1189
+ return classified;
1190
+ }
1191
+ // Async/generator calls hand back a promise or an iterator object.
1192
+ if (functionLike.async || functionLike.generator) {
1193
+ return 'nonPrimitive';
1194
+ }
1195
+ if (functionLike.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
1196
+ functionLike.expression) {
1197
+ return classifyReturnExpression(functionLike.body);
1198
+ }
1199
+ if (functionLike.body?.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
1200
+ return 'indeterminate';
1201
+ }
1202
+ const returnArguments = collectReturnArgumentsOf(functionLike.body);
1203
+ if (returnArguments.length === 0) {
1204
+ // Falls off the end: yields undefined.
1205
+ return 'primitive';
1206
+ }
1207
+ const combined = combineReturnEvaluations(returnArguments.map((arg) => classifyReturnExpression(arg)));
1208
+ // An unclassifiable return is still safe when no return expression can
1209
+ // syntactically reach a whole-parameter binding: the argument's reference
1210
+ // then has no path out through the result.
1211
+ if (combined === 'indeterminate' &&
1212
+ !returnArguments.some((arg) => expressionReachesParameters(arg, functionLike))) {
1213
+ return 'primitive';
1214
+ }
1215
+ return combined;
1216
+ }
1217
+ function parameterNamesOf(functionLike) {
1218
+ // Only a whole-parameter binding can carry the argument object's identity
1219
+ // out. A destructured binding holds a PROPERTY of the argument, so
1220
+ // returning it never exposes the literal's own identity.
1221
+ const names = new Set();
1222
+ const unwrap = (node) => {
1223
+ if (node.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
1224
+ return unwrap(node.left);
1225
+ }
1226
+ if (node.type === utils_1.AST_NODE_TYPES.RestElement) {
1227
+ return unwrap(node.argument);
1228
+ }
1229
+ if (node.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
1230
+ return unwrap(node.parameter);
1231
+ }
1232
+ return node;
1233
+ };
1234
+ for (const param of functionLike.params) {
1235
+ const bare = unwrap(param);
1236
+ if (bare.type === utils_1.AST_NODE_TYPES.Identifier) {
1237
+ names.add(bare.name);
1238
+ }
1239
+ }
1240
+ return names;
1241
+ }
1242
+ function expressionReachesParameters(expression, functionLike) {
1243
+ if (!expression)
1244
+ return false;
1245
+ const names = parameterNamesOf(functionLike);
1246
+ let reaches = false;
1247
+ const walk = (node) => {
1248
+ if (reaches)
1249
+ return;
1250
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
1251
+ // `arguments` exposes the raw argument list, so it can carry the
1252
+ // literal's identity out even when every parameter is destructured.
1253
+ (names.has(node.name) || node.name === 'arguments')) {
1254
+ reaches = true;
1255
+ return;
1256
+ }
1257
+ for (const key of Object.keys(node)) {
1258
+ if (key === 'parent')
1259
+ continue;
1260
+ const value = node[key];
1261
+ if (Array.isArray(value)) {
1262
+ for (const entry of value) {
1263
+ if (ASTHelpers_1.ASTHelpers.isNode(entry))
1264
+ walk(entry);
1265
+ }
1266
+ }
1267
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
1268
+ walk(value);
1269
+ }
1270
+ }
1271
+ };
1272
+ walk(expression);
1273
+ return reaches;
1274
+ }
1275
+ function functionOfDefinition(definition) {
1276
+ if (definition.type === 'FunctionName') {
1277
+ return definition.node;
1278
+ }
1279
+ if (definition.type === 'Variable') {
1280
+ const init = definition.node
1281
+ .init;
1282
+ if (init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
1283
+ init?.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
1284
+ return init;
1285
+ }
1286
+ }
1287
+ return undefined;
1288
+ }
1289
+ /**
1290
+ * True when every declaration bound to `calleeName` provably returns a
1291
+ * primitive, so invoking it cannot hand an argument's reference back.
1292
+ *
1293
+ * The analysis never follows a call inside the callee's body — a returned
1294
+ * CallExpression classifies as indeterminate — so it terminates on
1295
+ * self-recursive and mutually recursive callees without a re-entrancy guard.
1296
+ */
1297
+ function calleeReturnIsPrimitive(calleeName, startScope) {
1298
+ const variable = findVariableInScopes(calleeName, startScope);
1299
+ if (!variable || variable.defs.length === 0)
1300
+ return false;
1301
+ // A binding written again after initialization may hold a different
1302
+ // function by the time the call runs, so the declaration on record no
1303
+ // longer proves anything about the value actually invoked.
1304
+ if (variable.references.some((ref) => ref.isWrite() && !ref.init)) {
1305
+ return false;
1306
+ }
1307
+ const classifications = [];
1308
+ for (const definition of variable.defs) {
1309
+ const functionLike = functionOfDefinition(definition);
1310
+ // Imports, parameters and aliases hide the implementation.
1311
+ if (!functionLike)
1312
+ return false;
1313
+ classifications.push(classifyFunctionReturn(functionLike));
1314
+ }
1315
+ return combineReturnEvaluations(classifications) === 'primitive';
1316
+ }
865
1317
  /**
866
1318
  * True when `expr` — a plain function call's result, or a reference to the
867
1319
  * variable holding that result — is consumed only for its primitive value,
@@ -931,14 +1383,22 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
931
1383
  }
932
1384
  }
933
1385
  /**
934
- * True when the literal is a direct argument of a plain function call whose
935
- * result is only ever consumed primitively (see isPrimitivelyConsumed). In
936
- * that case the literal's identity provably never reaches a memoization
937
- * boundary — it is neither a JSX prop, nor a hook dependency, nor captured
938
- * by an effect — so re-creating it each render costs nothing and memoizing
939
- * it buys nothing. The callee must be a plain Identifier: member calls
940
- * (`obj.method({...})`) are excluded because the receiver could retain the
941
- * reference, and this keeps the guard to plain, non-hook synchronous calls.
1386
+ * True when the literal is a direct argument of a plain function call and
1387
+ * its identity provably cannot reach a memoization boundary — it is then
1388
+ * neither a JSX prop, nor a hook dependency, nor captured by an effect, so
1389
+ * re-creating it each render costs nothing and memoizing it buys nothing.
1390
+ *
1391
+ * Two independent conditions each suffice. The result is only ever consumed
1392
+ * primitively (see isPrimitivelyConsumed), which reasons from the CALL SITE.
1393
+ * Or the callee is locally declared and cannot hand the reference back (see
1394
+ * calleeReturnIsPrimitive), which reasons from the CALLEE. The call-site
1395
+ * condition covers callees that are imported or otherwise opaque; the callee
1396
+ * condition covers results consumed in positions no whitelist can prove
1397
+ * primitive, such as an object member or a hook dependency array.
1398
+ *
1399
+ * The callee must be a plain Identifier: member calls (`obj.method({...})`)
1400
+ * are excluded because the receiver could retain the reference, and this
1401
+ * keeps the guard to plain, non-hook synchronous calls.
942
1402
  */
943
1403
  function isPrimitiveConsumedCallArgument(node) {
944
1404
  // Walk up through transparent wrappers to the position the literal
@@ -962,7 +1422,12 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
962
1422
  if (parent.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
963
1423
  return false;
964
1424
  }
965
- return isPrimitivelyConsumed(parent);
1425
+ // Either sufficient condition exempts: a provably primitive consumption
1426
+ // site, or a callee that cannot hand the reference back.
1427
+ if (isPrimitivelyConsumed(parent)) {
1428
+ return true;
1429
+ }
1430
+ return calleeReturnIsPrimitive(parent.callee.name, scopeOf(node));
966
1431
  }
967
1432
  function reportLiteral(node) {
968
1433
  const descriptor = getLiteralDescriptor(node);
@@ -977,6 +1442,12 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
977
1442
  if (isTerminalUsage(node)) {
978
1443
  return;
979
1444
  }
1445
+ // A jest module factory builds a replacement module; React never renders
1446
+ // it, and jest's out-of-scope-variable restriction makes the rule's own
1447
+ // remediations illegal inside it.
1448
+ if (isInsideJestMockFactory(node)) {
1449
+ return;
1450
+ }
980
1451
  // A function literal passed directly as the callback to an Array
981
1452
  // iteration method (.map/.filter/.reduce/.forEach/...) is invoked
982
1453
  // synchronously during render and then discarded, so its identity is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.0",
3
+ "version": "1.20.2",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,49 @@
1
1
  [
2
+ {
3
+ "version": "1.20.2",
4
+ "date": "2026-07-25T02:42:18.863Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-unnecessary-verb-suffix",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1350
11
+ ],
12
+ "summary": "exempt member names dictated by a declared contract (closes #1350)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.1",
18
+ "date": "2026-07-24T22:02:37.444Z",
19
+ "rules": [
20
+ {
21
+ "name": "enforce-boolean-naming-prefixes",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1346
25
+ ],
26
+ "summary": "skip boolean-prefixed callees whose return is demonstrably non-boolean (closes #1346)"
27
+ },
28
+ {
29
+ "name": "enforce-firestore-facade",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1348
33
+ ],
34
+ "summary": "classify batches by import origin, not variable name (closes #1348)"
35
+ },
36
+ {
37
+ "name": "react-memoize-literals",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 1347,
41
+ 1349
42
+ ],
43
+ "summary": "exempt argument literals whose callee cannot return their reference (closes #1349); skip hook names resolved inside jest.mock and factory callbacks (closes #1347)"
44
+ }
45
+ ]
46
+ },
2
47
  {
3
48
  "version": "1.20.0",
4
49
  "date": "2026-07-24T15:36:40.953Z",