@blumintinc/eslint-plugin-blumint 1.19.26 → 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
|
@@ -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
|
|
@@ -178,6 +178,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
178
178
|
* name.
|
|
179
179
|
*/
|
|
180
180
|
const GUARD_PATTERN = /^(assert|ensure|require|validate|verify|guard|check)/i;
|
|
181
|
+
/**
|
|
182
|
+
* Matches state re-read callees by their leading verb. A refetch/refresh
|
|
183
|
+
* that follows a preceding await exists to re-observe state a prior await
|
|
184
|
+
* may have mutated (e.g. `await unlinkProvider(...)` then `await
|
|
185
|
+
* refreshUser()`, where refreshUser re-reads the post-unlink server state).
|
|
186
|
+
* Running such a pair through Promise.all races the refetch ahead of / in
|
|
187
|
+
* parallel with the mutation, so a refresh that resolves first repopulates
|
|
188
|
+
* the just-mutated state -- a genuine correctness bug. Anchored at the
|
|
189
|
+
* start so it fires on the callee's own verb (refreshUser, reloadData,
|
|
190
|
+
* refetchProfile) rather than on an arbitrary substring elsewhere in the
|
|
191
|
+
* name (getRefreshToken must NOT match).
|
|
192
|
+
*/
|
|
193
|
+
const REFETCH_PATTERN = /^(refresh|reload|refetch|revalidate|resync|sync)/i;
|
|
181
194
|
/**
|
|
182
195
|
* Extracts the callee's method name (the identifier bearing the leading
|
|
183
196
|
* verb) from an await expression argument. Handles both direct
|
|
@@ -324,6 +337,33 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
324
337
|
return true;
|
|
325
338
|
}
|
|
326
339
|
}
|
|
340
|
+
// 5. Refetch/refresh ordering barrier. A discarded-result await whose
|
|
341
|
+
// callee reads as a state re-read (refresh*, reload*, refetch*,
|
|
342
|
+
// revalidate*, resync*, sync*) exists to observe state that a PRECEDING
|
|
343
|
+
// await may have mutated. Promise.all invokes every operand eagerly and
|
|
344
|
+
// concurrently, so it would race the refetch ahead of the mutation -- a
|
|
345
|
+
// refresh that resolves first repopulates the just-mutated state, a
|
|
346
|
+
// genuine correctness bug. Treat a refetch that FOLLOWS another await as
|
|
347
|
+
// a sequencing dependency that blocks parallelizing the run. Only
|
|
348
|
+
// non-first positions qualify: the preceding await is what the refetch
|
|
349
|
+
// depends on. getCalleeMethodName handles both bare-identifier and
|
|
350
|
+
// member-expression callees. Only discarded-result awaits
|
|
351
|
+
// (ExpressionStatements) qualify here; a captured result carries a value
|
|
352
|
+
// and is handled by the data-dependency path above.
|
|
353
|
+
for (let i = 1; i < awaitNodes.length; i++) {
|
|
354
|
+
const node = awaitNodes[i];
|
|
355
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const awaitExpr = getAwaitExpression(node);
|
|
359
|
+
if (!awaitExpr) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const methodName = getCalleeMethodName(awaitExpr);
|
|
363
|
+
if (methodName && REFETCH_PATTERN.test(methodName)) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
327
367
|
// If any node is a variable declaration with destructuring, consider it as having dependencies
|
|
328
368
|
for (const node of awaitNodes) {
|
|
329
369
|
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
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
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.27",
|
|
18
|
+
"date": "2026-07-23T04:26:05.419Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "parallelize-async-operations",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1334
|
|
25
|
+
],
|
|
26
|
+
"summary": "treat mutation→refetch bare-identifier awaits as ordered (closes #1334)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.26",
|
|
4
32
|
"date": "2026-07-23T00:25:08.850Z",
|