@blumintinc/eslint-plugin-blumint 1.19.22 → 1.19.23

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 CHANGED
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.22',
225
+ version: '1.19.23',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -86,6 +86,21 @@ const ITERATION_METHODS = new Set([
86
86
  'flatMap',
87
87
  'sort',
88
88
  ]);
89
+ /**
90
+ * Comparison operators whose result is a boolean primitive. A value produced by
91
+ * one of these can never carry a referential identity, so a call whose result
92
+ * feeds such a comparison never lets its argument's identity escape.
93
+ */
94
+ const COMPARISON_OPERATORS = new Set([
95
+ '===',
96
+ '!==',
97
+ '==',
98
+ '!=',
99
+ '<',
100
+ '>',
101
+ '<=',
102
+ '>=',
103
+ ]);
89
104
  const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
90
105
  const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
91
106
  const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
@@ -847,6 +862,108 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
847
862
  }
848
863
  return usages.every((ref) => isStyleJSXAttributeValue(ref.identifier));
849
864
  }
865
+ /**
866
+ * True when `expr` — a plain function call's result, or a reference to the
867
+ * variable holding that result — is consumed only for its primitive value,
868
+ * never by reference.
869
+ *
870
+ * Referential stability of a call's ARGUMENT matters only if the call's
871
+ * RESULT reaches a memoization boundary (a hook dependency, a memoized
872
+ * child's prop, an effect capture) where identities are reference-compared.
873
+ * A result that lands in a boolean-test position — a ternary/if/while/for
874
+ * test, a `!`, a comparison, or a logical chain that itself ends in such a
875
+ * position — is coerced to (or already is) a primitive, so the argument's
876
+ * identity provably never crosses such a boundary regardless of what the
877
+ * callee returns. Reasoning from how the CALLER consumes the result (not the
878
+ * callee's body) keeps the analysis purely syntactic and type-free.
879
+ *
880
+ * The whitelist is deliberately tight: any position not proven primitive
881
+ * (JSX attribute value, hook dependency element, argument to another call,
882
+ * spread, return, object/array member) yields false, so the exemption never
883
+ * silences a literal whose identity could still escape.
884
+ */
885
+ function isPrimitivelyConsumed(expr) {
886
+ const parent = expr.parent;
887
+ if (!parent) {
888
+ return false;
889
+ }
890
+ // TS-assertion/parenthesized wrappers are transparent to consumption:
891
+ // analyze how the wrapped value is ultimately used.
892
+ if (isExpressionWrapper(parent)) {
893
+ const wrapper = parent;
894
+ return wrapper.expression === expr && isPrimitivelyConsumed(parent);
895
+ }
896
+ switch (parent.type) {
897
+ case utils_1.AST_NODE_TYPES.ConditionalExpression:
898
+ return parent.test === expr;
899
+ case utils_1.AST_NODE_TYPES.IfStatement:
900
+ case utils_1.AST_NODE_TYPES.WhileStatement:
901
+ case utils_1.AST_NODE_TYPES.DoWhileStatement:
902
+ case utils_1.AST_NODE_TYPES.ForStatement:
903
+ return parent.test === expr;
904
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
905
+ return parent.operator === '!';
906
+ case utils_1.AST_NODE_TYPES.BinaryExpression:
907
+ return COMPARISON_OPERATORS.has(parent.operator);
908
+ case utils_1.AST_NODE_TYPES.LogicalExpression:
909
+ // A logical operand inherits the consumption of the whole expression:
910
+ // safe only if that ultimately lands in a primitive position too.
911
+ return isPrimitivelyConsumed(parent);
912
+ case utils_1.AST_NODE_TYPES.VariableDeclarator: {
913
+ if (parent.init !== expr ||
914
+ parent.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
915
+ return false;
916
+ }
917
+ const variables = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, parent);
918
+ if (variables.length === 0) {
919
+ return false;
920
+ }
921
+ const usages = variables[0].references.filter((ref) => !ref.init);
922
+ // No usages (dead code): can't prove the result stays primitive, so
923
+ // keep the literal reported (mirrors isStyleVariableInitializer).
924
+ if (usages.length === 0) {
925
+ return false;
926
+ }
927
+ return usages.every((ref) => isPrimitivelyConsumed(ref.identifier));
928
+ }
929
+ default:
930
+ return false;
931
+ }
932
+ }
933
+ /**
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.
942
+ */
943
+ function isPrimitiveConsumedCallArgument(node) {
944
+ // Walk up through transparent wrappers to the position the literal
945
+ // effectively occupies as a call argument.
946
+ let effective = node;
947
+ let parent = effective.parent;
948
+ while (parent &&
949
+ isExpressionWrapper(parent) &&
950
+ parent
951
+ .expression === effective) {
952
+ effective = parent;
953
+ parent = effective.parent;
954
+ }
955
+ if (!parent || parent.type !== utils_1.AST_NODE_TYPES.CallExpression) {
956
+ return false;
957
+ }
958
+ const isDirectArgument = parent.arguments.some((arg) => arg === effective);
959
+ if (!isDirectArgument) {
960
+ return false;
961
+ }
962
+ if (parent.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
963
+ return false;
964
+ }
965
+ return isPrimitivelyConsumed(parent);
966
+ }
850
967
  function reportLiteral(node) {
851
968
  const descriptor = getLiteralDescriptor(node);
852
969
  if (!descriptor)
@@ -937,6 +1054,14 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
937
1054
  });
938
1055
  return;
939
1056
  }
1057
+ // A literal passed only as an argument to a plain synchronous call whose
1058
+ // result is consumed primitively never carries its identity to a
1059
+ // memoization boundary, so memoizing it is pointless (issue #1329). Placed
1060
+ // last so hook-argument and hook-return handling run first and are
1061
+ // unaffected.
1062
+ if (isPrimitiveConsumedCallArgument(node)) {
1063
+ return;
1064
+ }
940
1065
  const contextLabel = formatContextLabel(owner);
941
1066
  // Only emit auto-fix suggestions for simple variable initializers; other
942
1067
  // contexts (returns, JSX props, nested expressions) risk unsafe rewrites.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.22",
3
+ "version": "1.19.23",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.19.23",
4
+ "date": "2026-07-22T15:32:41.825Z",
5
+ "rules": [
6
+ {
7
+ "name": "react-memoize-literals",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1329
11
+ ],
12
+ "summary": "exempt literal args to primitively-consumed plain calls (closes #1329)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.19.22",
4
18
  "date": "2026-07-22T09:39:07.911Z",