@blumintinc/eslint-plugin-blumint 1.19.27 → 1.19.28

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.27',
225
+ version: '1.19.28',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -339,6 +339,35 @@ const returnExpressionIsJsx = (expression) => {
339
339
  return (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
340
340
  unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment);
341
341
  };
342
+ /**
343
+ * True when the expression is a `memo(...)` / `forwardRef(...)` call. Returned
344
+ * from a useMemo/useDeepCompareMemo callback, this is the complete, sanctioned
345
+ * stabilization pattern: the memo hook stabilizes the component *identity*
346
+ * across re-renders (a new identity only on a deps change), so the inner
347
+ * component never remounts on an ordinary re-render—the exact harm this rule
348
+ * exists to prevent does not occur.
349
+ */
350
+ const returnExpressionIsMemoOrForwardRefCall = (expression, reactImports) => {
351
+ if (!expression) {
352
+ return false;
353
+ }
354
+ const unwrapped = unwrapExpression(expression);
355
+ return (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression &&
356
+ (isMemoCall(unwrapped, reactImports) ||
357
+ isForwardRefCall(unwrapped, reactImports)));
358
+ };
359
+ /**
360
+ * True when a useMemo/useDeepCompareMemo callback directly returns a
361
+ * memo()/forwardRef()-wrapped component. The memo hook stabilizes the returned
362
+ * component's identity, so such a binding does not remount on re-render and
363
+ * must not be flagged.
364
+ */
365
+ const memoCallbackReturnsMemoizedComponent = (callback, reactImports) => {
366
+ if (!isFunctionExpression(callback)) {
367
+ return false;
368
+ }
369
+ return collectDirectReturnExpressions(callback).some((expression) => returnExpressionIsMemoOrForwardRefCall(expression, reactImports));
370
+ };
342
371
  /**
343
372
  * Direct return-statement arguments of a function, traversing control flow
344
373
  * (if/switch/try/loops) but NOT descending into nested function definitions—
@@ -486,6 +515,12 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
486
515
  // Returns JSX directly, so it's an element, not a component
487
516
  return;
488
517
  }
518
+ // A memo()/forwardRef()-wrapped return is identity-stabilized by the
519
+ // memo hook, so it does not remount on re-render and must not be
520
+ // flagged (see #1336). Bare inner components stay flagged.
521
+ if (memoCallbackReturnsMemoizedComponent(callback, reactImports)) {
522
+ return;
523
+ }
489
524
  }
490
525
  const variableName = getVariableName(node);
491
526
  // A non-PascalCase binding (e.g. renderHit) is a render callback used
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.27",
3
+ "version": "1.19.28",
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.28",
4
+ "date": "2026-07-23T17:25:39.744Z",
5
+ "rules": [
6
+ {
7
+ "name": "memo-nested-react-components",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1336
11
+ ],
12
+ "summary": "exempt memo/forwardRef components returned from useMemo/useDeepCompareMemo callbacks (closes #1336)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.19.27",
4
18
  "date": "2026-07-23T04:26:05.419Z",