oxlint-plugin-react-doctor 0.7.9-dev.2ba83c3 → 0.7.9-dev.4f3848b
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/dist/index.d.ts +46 -0
- package/dist/index.js +1322 -120
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1237,12 +1237,13 @@ const stripParenExpression = (node) => {
|
|
|
1237
1237
|
};
|
|
1238
1238
|
//#endregion
|
|
1239
1239
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
1240
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
1240
|
+
const resolveConstIdentifierAlias = (identifier, scopes, allowPatternBinding = false) => {
|
|
1241
1241
|
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
1242
1242
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
1243
1243
|
let symbol = scopes.symbolFor(identifier);
|
|
1244
1244
|
while (symbol?.kind === "const") {
|
|
1245
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")
|
|
1245
|
+
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return null;
|
|
1246
|
+
if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
|
|
1246
1247
|
visitedSymbolIds.add(symbol.id);
|
|
1247
1248
|
const initializer = stripParenExpression(symbol.initializer);
|
|
1248
1249
|
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
@@ -8959,7 +8960,7 @@ const isReactNamespaceImport = (identifier, scopes) => {
|
|
|
8959
8960
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8960
8961
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8961
8962
|
};
|
|
8962
|
-
const isReactNamespaceReceiver = (receiver, scopes, options) => {
|
|
8963
|
+
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
8963
8964
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
8964
8965
|
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
8965
8966
|
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
@@ -8972,7 +8973,7 @@ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) =>
|
|
|
8972
8973
|
for (const property of pattern.properties) {
|
|
8973
8974
|
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
8974
8975
|
const propertyName = getStaticPropertyKeyName(property);
|
|
8975
|
-
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver(stripParenExpression(symbol.initializer), scopes, options));
|
|
8976
|
+
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
8976
8977
|
}
|
|
8977
8978
|
return false;
|
|
8978
8979
|
};
|
|
@@ -8996,7 +8997,7 @@ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds
|
|
|
8996
8997
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
8997
8998
|
}
|
|
8998
8999
|
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
8999
|
-
return isReactNamespaceReceiver(stripParenExpression(callee.object), scopes, options);
|
|
9000
|
+
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
9000
9001
|
};
|
|
9001
9002
|
//#endregion
|
|
9002
9003
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
@@ -13652,7 +13653,7 @@ const getPromiseChainCallForCallback = (candidate) => {
|
|
|
13652
13653
|
if (!callbackContainer.arguments?.some((argument) => stripParenExpression(argument) === candidate)) return null;
|
|
13653
13654
|
return isPromiseChainCall(stripParenExpression(callbackContainer.callee)) ? callbackContainer : null;
|
|
13654
13655
|
};
|
|
13655
|
-
const
|
|
13656
|
+
const collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
|
|
13656
13657
|
const invokedFunctions = new Set([effectCallback]);
|
|
13657
13658
|
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
13658
13659
|
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
@@ -13686,12 +13687,14 @@ const collectEffectInvokedFunctions = (effectCallback) => {
|
|
|
13686
13687
|
calledBindingNames.add(callee.name);
|
|
13687
13688
|
return;
|
|
13688
13689
|
}
|
|
13689
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
13690
|
+
if (includePromiseCallbacks && isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
13690
13691
|
});
|
|
13691
13692
|
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
13692
13693
|
}
|
|
13693
13694
|
return invokedFunctions;
|
|
13694
13695
|
};
|
|
13696
|
+
const collectEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, true);
|
|
13697
|
+
const collectSynchronouslyEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, false);
|
|
13695
13698
|
//#endregion
|
|
13696
13699
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
13697
13700
|
const isReactHookName = (name) => {
|
|
@@ -13850,15 +13853,19 @@ const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
|
13850
13853
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
13851
13854
|
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
13852
13855
|
};
|
|
13853
|
-
const
|
|
13856
|
+
const resolveReactRefCurrentOriginSymbol = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13854
13857
|
const expression = stripParenExpression(node);
|
|
13855
|
-
|
|
13856
|
-
if (
|
|
13857
|
-
|
|
13858
|
-
|
|
13858
|
+
const refSymbol = resolveReactRefSymbol(expression, scopes);
|
|
13859
|
+
if (refSymbol) return refSymbol;
|
|
13860
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
13861
|
+
const symbol = scopes.symbolFor(expression);
|
|
13862
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
|
|
13863
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
13864
|
+
if (!initializer) return null;
|
|
13859
13865
|
visitedSymbolIds.add(symbol.id);
|
|
13860
|
-
return
|
|
13866
|
+
return resolveReactRefCurrentOriginSymbol(initializer, scopes, visitedSymbolIds);
|
|
13861
13867
|
};
|
|
13868
|
+
const hasReactRefCurrentOrigin = (node, scopes) => resolveReactRefCurrentOriginSymbol(node, scopes) !== null;
|
|
13862
13869
|
//#endregion
|
|
13863
13870
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
13864
13871
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -14340,6 +14347,39 @@ const findContainingCollectionKey = (resourceNode, context) => {
|
|
|
14340
14347
|
}
|
|
14341
14348
|
return null;
|
|
14342
14349
|
};
|
|
14350
|
+
const findPushedResourceCollectionKey = (usage, context) => {
|
|
14351
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return null;
|
|
14352
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
14353
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed) return null;
|
|
14354
|
+
const resourceIdentifier = stripParenExpression(registrationCallee.object);
|
|
14355
|
+
if (!isPrivatePlainConstIdentifier(resourceIdentifier, context)) return null;
|
|
14356
|
+
const resourceSymbol = context.scopes.symbolFor(resourceIdentifier);
|
|
14357
|
+
if (!resourceSymbol) return null;
|
|
14358
|
+
const pushCalls = resourceSymbol.references.flatMap((reference) => {
|
|
14359
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14360
|
+
const callNode = referenceRoot.parent;
|
|
14361
|
+
if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((argument) => argument === referenceRoot)) return [];
|
|
14362
|
+
const pushCallee = stripParenExpression(callNode.callee);
|
|
14363
|
+
return isNodeOfType(pushCallee, "MemberExpression") && !pushCallee.computed && isNodeOfType(pushCallee.object, "Identifier") && isNodeOfType(pushCallee.property, "Identifier") && pushCallee.property.name === "push" ? [callNode] : [];
|
|
14364
|
+
});
|
|
14365
|
+
if (pushCalls.length !== 1) return null;
|
|
14366
|
+
const pushCall = pushCalls[0];
|
|
14367
|
+
if (findEnclosingFunction$1(pushCall) !== findEnclosingFunction$1(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [pushCall], context)) return null;
|
|
14368
|
+
const pushCallee = stripParenExpression(pushCall.callee);
|
|
14369
|
+
if (!isNodeOfType(pushCallee, "MemberExpression") || !isNodeOfType(pushCallee.object, "Identifier") || !isPrivatePlainConstIdentifier(pushCallee.object, context)) return null;
|
|
14370
|
+
const collectionSymbol = context.scopes.symbolFor(pushCallee.object);
|
|
14371
|
+
const collectionInitializer = collectionSymbol?.initializer ? stripParenExpression(collectionSymbol.initializer) : null;
|
|
14372
|
+
if (!collectionSymbol || !isNodeOfType(collectionInitializer, "ArrayExpression") || (collectionInitializer.elements?.length ?? 0) !== 0 || findEnclosingFunction$1(collectionSymbol.declarationNode) !== findEnclosingFunction$1(usage.node)) return null;
|
|
14373
|
+
return collectionSymbol.references.every((reference) => {
|
|
14374
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14375
|
+
const forOfStatement = referenceRoot.parent;
|
|
14376
|
+
if (isNodeOfType(forOfStatement, "ForOfStatement") && forOfStatement.right === referenceRoot && forOfStatement.await !== true) return true;
|
|
14377
|
+
const memberNode = referenceRoot.parent;
|
|
14378
|
+
const callNode = memberNode?.parent;
|
|
14379
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== referenceRoot || memberNode.computed || !isNodeOfType(memberNode.property, "Identifier") || !isNodeOfType(callNode, "CallExpression") || callNode.callee !== memberNode) return false;
|
|
14380
|
+
return memberNode.property.name === "forEach" || memberNode.property.name === "push";
|
|
14381
|
+
}) ? resolveExpressionKey(pushCallee.object, context) : null;
|
|
14382
|
+
};
|
|
14343
14383
|
const isWithinAssignmentTarget = (identifier) => {
|
|
14344
14384
|
let currentNode = identifier;
|
|
14345
14385
|
let parentNode = currentNode.parent;
|
|
@@ -14398,6 +14438,14 @@ const isSynchronousIteratorCallback = (functionNode) => {
|
|
|
14398
14438
|
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
14399
14439
|
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
14400
14440
|
};
|
|
14441
|
+
const findEnclosingForEachCall = (node) => {
|
|
14442
|
+
const callbackNode = findEnclosingFunction$1(node);
|
|
14443
|
+
if (!callbackNode) return null;
|
|
14444
|
+
const callNode = callbackNode.parent;
|
|
14445
|
+
if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
|
|
14446
|
+
const callee = stripParenExpression(callNode.callee);
|
|
14447
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" ? callNode : null;
|
|
14448
|
+
};
|
|
14401
14449
|
const findDirectCallForReference = (identifier) => {
|
|
14402
14450
|
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
14403
14451
|
const callNode = expressionRoot.parent;
|
|
@@ -14412,7 +14460,7 @@ const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
|
14412
14460
|
const callNode = findDirectCallForReference(reference.identifier);
|
|
14413
14461
|
return callNode ? [callNode] : [];
|
|
14414
14462
|
});
|
|
14415
|
-
if (invocationCalls.length !== 1) return null;
|
|
14463
|
+
if (invocationCalls.length !== 1 || symbol.references.length !== 1) return null;
|
|
14416
14464
|
const invocationCall = invocationCalls[0];
|
|
14417
14465
|
return findEnclosingFunction$1(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
14418
14466
|
};
|
|
@@ -14446,7 +14494,15 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
14446
14494
|
if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
14447
14495
|
const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
14448
14496
|
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
14449
|
-
const
|
|
14497
|
+
const cleanupForEachCall = findEnclosingForEachCall(cleanupChild);
|
|
14498
|
+
const cleanupCallee = isNodeOfType(cleanupCall, "CallExpression") ? stripParenExpression(cleanupCall.callee) : null;
|
|
14499
|
+
const cleanupReceiverForOfStatement = isNodeOfType(cleanupCallee, "MemberExpression") ? findForOfStatementForIteratorExpression(cleanupCallee.object, context) : null;
|
|
14500
|
+
const cleanupReceiverCollectionKey = cleanupReceiverForOfStatement ? resolveExpressionKey(cleanupReceiverForOfStatement.right, context) : isNodeOfType(cleanupCallee, "MemberExpression") ? resolveIteratorCollectionKey(cleanupCallee.object, context) : null;
|
|
14501
|
+
if (cleanupReceiverCollectionKey !== null && findEnclosingFunction$1(cleanupChild) !== cleanupFunction) {
|
|
14502
|
+
if (cleanupForEachCall && findPushedResourceCollectionKey(usage, context) === cleanupReceiverCollectionKey) matchingLoopOrHelperAnchors.push(cleanupForEachCall);
|
|
14503
|
+
return;
|
|
14504
|
+
}
|
|
14505
|
+
const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context) ?? cleanupReceiverForOfStatement;
|
|
14450
14506
|
if (!cleanupForOfStatement) {
|
|
14451
14507
|
didCleanupFunctionMatch = true;
|
|
14452
14508
|
return false;
|
|
@@ -14490,28 +14546,28 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
|
14490
14546
|
});
|
|
14491
14547
|
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
14492
14548
|
};
|
|
14493
|
-
const
|
|
14494
|
-
if (
|
|
14495
|
-
const
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
const unwrappedOperand = stripParenExpression(operand);
|
|
14501
|
-
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
14502
|
-
};
|
|
14503
|
-
return resolveExpressionKey(unwrappedTest.left, context) === usage.handleKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === usage.handleKey && isNullishOperand(unwrappedTest.left);
|
|
14549
|
+
const doesTestRequireLiveExpressionKey = (test, expressionKey, context) => {
|
|
14550
|
+
if (resolveExpressionKey(test, context) === expressionKey) return true;
|
|
14551
|
+
const unwrappedTest = stripParenExpression(test);
|
|
14552
|
+
if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
|
|
14553
|
+
const isNullishOperand = (operand) => {
|
|
14554
|
+
const unwrappedOperand = stripParenExpression(operand);
|
|
14555
|
+
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
14504
14556
|
};
|
|
14557
|
+
return resolveExpressionKey(unwrappedTest.left, context) === expressionKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === expressionKey && isNullishOperand(unwrappedTest.left);
|
|
14558
|
+
};
|
|
14559
|
+
const findLiveExpressionGuardForRelease = (releaseCall, owner, expressionKey, context) => {
|
|
14505
14560
|
let ancestor = releaseCall.parent;
|
|
14506
14561
|
while (ancestor && ancestor !== owner) {
|
|
14507
14562
|
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
14508
|
-
if (ancestor.alternate !== null || !
|
|
14563
|
+
if (ancestor.alternate !== null || !doesTestRequireLiveExpressionKey(ancestor.test, expressionKey, context) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
|
|
14509
14564
|
return ancestor;
|
|
14510
14565
|
}
|
|
14511
14566
|
ancestor = ancestor.parent;
|
|
14512
14567
|
}
|
|
14513
14568
|
return null;
|
|
14514
14569
|
};
|
|
14570
|
+
const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => usage.handleKey === null ? null : findLiveExpressionGuardForRelease(releaseCall, owner, usage.handleKey, context);
|
|
14515
14571
|
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
14516
14572
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
14517
14573
|
const functionCfg = context.cfg.cfgFor(callback);
|
|
@@ -14699,7 +14755,108 @@ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, con
|
|
|
14699
14755
|
});
|
|
14700
14756
|
return hasPotentialInterruption;
|
|
14701
14757
|
};
|
|
14758
|
+
const getNumericReactRefCurrentKey = (expression, context) => {
|
|
14759
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(expression), context.scopes);
|
|
14760
|
+
const initializer = refSymbol?.initializer ? stripParenExpression(refSymbol.initializer) : null;
|
|
14761
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
14762
|
+
const initialValue = initializer.arguments?.[0] ? stripParenExpression(initializer.arguments[0]) : null;
|
|
14763
|
+
if (!isNodeOfType(initialValue, "Literal") || typeof initialValue.value !== "number") return null;
|
|
14764
|
+
return resolveExpressionKey(expression, context);
|
|
14765
|
+
};
|
|
14766
|
+
const getBlockingGenerationKey = (expression, context) => {
|
|
14767
|
+
const test = stripParenExpression(expression);
|
|
14768
|
+
if (isNodeOfType(test, "LogicalExpression") && test.operator === "||") return getBlockingGenerationKey(test.left, context) ?? getBlockingGenerationKey(test.right, context);
|
|
14769
|
+
if (!isNodeOfType(test, "BinaryExpression") || test.operator !== "!==" && test.operator !== "!=") return null;
|
|
14770
|
+
const leftKey = getNumericReactRefCurrentKey(test.left, context);
|
|
14771
|
+
const rightKey = getNumericReactRefCurrentKey(test.right, context);
|
|
14772
|
+
const snapshotExpression = leftKey ? stripParenExpression(test.right) : stripParenExpression(test.left);
|
|
14773
|
+
const key = leftKey ?? rightKey;
|
|
14774
|
+
return key && isNodeOfType(snapshotExpression, "Identifier") ? key : null;
|
|
14775
|
+
};
|
|
14776
|
+
const findGenerationGuardKeyForDeferredUsage = (usageFunction, usageNode, context) => {
|
|
14777
|
+
if (!isFunctionLike$1(usageFunction)) return null;
|
|
14778
|
+
let generationKey = null;
|
|
14779
|
+
walkAst(usageFunction.body, (child) => {
|
|
14780
|
+
if (generationKey) return false;
|
|
14781
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
14782
|
+
if (!isNodeOfType(child, "IfStatement") || child.alternate) return;
|
|
14783
|
+
const key = getBlockingGenerationKey(child.test, context);
|
|
14784
|
+
if (!key || canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, usageFunction, context) || !doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], usageFunction, context)) return;
|
|
14785
|
+
generationKey = key;
|
|
14786
|
+
});
|
|
14787
|
+
return generationKey;
|
|
14788
|
+
};
|
|
14789
|
+
const isGenerationAdvance = (node, generationKey, context) => {
|
|
14790
|
+
if (isNodeOfType(node, "UpdateExpression") && resolveExpressionKey(node.argument, context) === generationKey) return true;
|
|
14791
|
+
if (!isNodeOfType(node, "AssignmentExpression") || resolveExpressionKey(node.left, context) !== generationKey || node.operator !== "+=" && node.operator !== "-=") return false;
|
|
14792
|
+
const amount = stripParenExpression(node.right);
|
|
14793
|
+
return isNodeOfType(amount, "Literal") && typeof amount.value === "number" && amount.value !== 0;
|
|
14794
|
+
};
|
|
14795
|
+
const functionAdvancesGeneration = (owner, generationKey, context) => {
|
|
14796
|
+
if (!isFunctionLike$1(owner)) return false;
|
|
14797
|
+
let didAdvanceGeneration = false;
|
|
14798
|
+
walkAst(owner.body, (child) => {
|
|
14799
|
+
if (didAdvanceGeneration) return false;
|
|
14800
|
+
if (child !== owner.body && isFunctionLike$1(child)) return false;
|
|
14801
|
+
if (isGenerationAdvance(child, generationKey, context)) {
|
|
14802
|
+
didAdvanceGeneration = true;
|
|
14803
|
+
return false;
|
|
14804
|
+
}
|
|
14805
|
+
});
|
|
14806
|
+
return didAdvanceGeneration;
|
|
14807
|
+
};
|
|
14808
|
+
const cleanupReturnsReleaseUsage = (cleanupReturns, usage, context) => cleanupReturns.length > 0 && cleanupReturns.every((cleanupReturn) => {
|
|
14809
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
|
|
14810
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
14811
|
+
return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
|
|
14812
|
+
});
|
|
14813
|
+
const getOwnedFunctionReference = (reference, usageFunction, usageNode, callback, cleanupReturns, context) => {
|
|
14814
|
+
const directCall = findDirectCallForReference(reference);
|
|
14815
|
+
if (directCall) {
|
|
14816
|
+
const referenceOwner = findEnclosingFunction$1(directCall);
|
|
14817
|
+
if (referenceOwner && referenceOwner !== usageFunction && collectSynchronouslyEffectInvokedFunctions(callback).has(referenceOwner)) return { generationKey: null };
|
|
14818
|
+
const generationKey = referenceOwner ? findGenerationGuardKeyForDeferredUsage(referenceOwner, directCall, context) : null;
|
|
14819
|
+
return generationKey ? { generationKey } : null;
|
|
14820
|
+
}
|
|
14821
|
+
const referenceRoot = findTransparentExpressionRoot(reference);
|
|
14822
|
+
const schedulerCall = referenceRoot.parent;
|
|
14823
|
+
if (!isNodeOfType(schedulerCall, "CallExpression") || !schedulerCall.arguments.some((argument) => argument === referenceRoot) || !isNodeOfType(schedulerCall.callee, "Identifier") || schedulerCall.callee.name !== "setTimeout" || !context.scopes.isGlobalReference(schedulerCall.callee)) return null;
|
|
14824
|
+
const schedulerUsage = {
|
|
14825
|
+
kind: "timer",
|
|
14826
|
+
node: schedulerCall,
|
|
14827
|
+
resourceName: schedulerCall.callee.name,
|
|
14828
|
+
handleKey: findAssignedResourceKey(schedulerCall, context),
|
|
14829
|
+
receiverKey: null,
|
|
14830
|
+
registrationVerbName: schedulerCall.callee.name,
|
|
14831
|
+
eventKey: null,
|
|
14832
|
+
handlerKey: null
|
|
14833
|
+
};
|
|
14834
|
+
const generationKey = findGenerationGuardKeyForDeferredUsage(usageFunction, usageNode, context);
|
|
14835
|
+
return schedulerUsage.handleKey !== null && cleanupReturnsReleaseUsage(cleanupReturns, schedulerUsage, context) && generationKey ? { generationKey } : null;
|
|
14836
|
+
};
|
|
14837
|
+
const hasGuardedRefOwnedNestedCleanup = (callback, usage, cleanupReturns, context) => {
|
|
14838
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14839
|
+
const usageExpression = findTransparentExpressionRoot(usage.node);
|
|
14840
|
+
const usageAssignment = usageExpression.parent;
|
|
14841
|
+
if (usage.kind !== "subscribe" && usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !resolveReactRefSymbol(stripParenExpression(usageAssignment.left), context.scopes) || !collectSynchronouslyEffectInvokedFunctions(callback).has(usageFunction) || !cleanupReturnsReleaseUsage(cleanupReturns, usage, context) || !doMatchingNodesCoverEveryPathFromFunctionEntry(callback, cleanupReturns, context)) return false;
|
|
14842
|
+
const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
|
|
14843
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
|
|
14844
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
14845
|
+
return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
|
|
14846
|
+
});
|
|
14847
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(usageFunction);
|
|
14848
|
+
const functionSymbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
14849
|
+
if (!functionSymbol || functionSymbol.references.length === 0) return false;
|
|
14850
|
+
const ownedReferences = functionSymbol.references.map((reference) => getOwnedFunctionReference(reference.identifier, usageFunction, usage.node, callback, cleanupReturns, context));
|
|
14851
|
+
if (ownedReferences.some((reference) => reference === null)) return false;
|
|
14852
|
+
const generationKeys = new Set(ownedReferences.flatMap((reference) => reference?.generationKey ? [reference.generationKey] : []));
|
|
14853
|
+
if (generationKeys.size !== 1) return false;
|
|
14854
|
+
const generationKey = generationKeys.values().next().value;
|
|
14855
|
+
if (typeof generationKey !== "string") return false;
|
|
14856
|
+
return [...collectSynchronouslyEffectInvokedFunctions(callback), ...cleanupFunctions].some((owner) => functionAdvancesGeneration(owner, generationKey, context));
|
|
14857
|
+
};
|
|
14702
14858
|
const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
|
|
14859
|
+
if (hasGuardedRefOwnedNestedCleanup(callback, usage, cleanupReturns, context)) return true;
|
|
14703
14860
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14704
14861
|
const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
|
|
14705
14862
|
if (usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usage.node, "CallExpression") || !isNodeOfType(usage.node.callee, "Identifier") || !context.scopes.isGlobalReference(usage.node.callee) || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
|
|
@@ -14870,7 +15027,7 @@ const getReleaseVerbName = (node) => {
|
|
|
14870
15027
|
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
14871
15028
|
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
14872
15029
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14873
|
-
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !
|
|
15030
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
|
|
14874
15031
|
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
14875
15032
|
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
14876
15033
|
if (controllerKey === null || refCurrentKey === null) return false;
|
|
@@ -14890,6 +15047,62 @@ const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) =>
|
|
|
14890
15047
|
const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
|
|
14891
15048
|
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
14892
15049
|
};
|
|
15050
|
+
const isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
|
|
15051
|
+
const isFunctionForwardedToReactRef = (functionNode, context) => {
|
|
15052
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15053
|
+
if (!bindingIdentifier) return false;
|
|
15054
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
15055
|
+
if (!symbol) return false;
|
|
15056
|
+
return symbol.references.some((reference) => {
|
|
15057
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15058
|
+
const expressionContainer = referenceRoot.parent;
|
|
15059
|
+
return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
|
|
15060
|
+
});
|
|
15061
|
+
};
|
|
15062
|
+
const isFunctionReturnedFromReactHook = (functionNode, context, requireRefPropertyName) => {
|
|
15063
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15064
|
+
if (!bindingIdentifier) return false;
|
|
15065
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
15066
|
+
if (!symbol) return false;
|
|
15067
|
+
return symbol.references.some((reference) => {
|
|
15068
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15069
|
+
const property = referenceRoot.parent;
|
|
15070
|
+
const propertyName = isNodeOfType(property, "Property") ? getStaticPropertyKeyName(property) : null;
|
|
15071
|
+
if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression") || requireRefPropertyName && propertyName !== "ref" && !propertyName?.endsWith("Ref")) return false;
|
|
15072
|
+
const returnedObject = findTransparentExpressionRoot(property.parent);
|
|
15073
|
+
const returnStatement = returnedObject.parent;
|
|
15074
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
|
|
15075
|
+
const ownerFunction = findEnclosingFunction$1(returnStatement);
|
|
15076
|
+
return Boolean(ownerFunction && isReactHookName(getFunctionBindingIdentifier$1(ownerFunction)?.name ?? ""));
|
|
15077
|
+
});
|
|
15078
|
+
};
|
|
15079
|
+
const isFunctionUsedAsReactRef = (functionNode, context) => isFunctionForwardedToReactRef(functionNode, context) || isFunctionReturnedFromReactHook(functionNode, context, true);
|
|
15080
|
+
const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
15081
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return false;
|
|
15082
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15083
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction) || usageFunction !== findEnclosingFunction$1(releaseCall) || !isFunctionUsedAsReactRef(usageFunction, context)) return false;
|
|
15084
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
15085
|
+
const releaseCallee = stripParenExpression(releaseCall.callee);
|
|
15086
|
+
const releaseRefSymbol = isNodeOfType(releaseCallee, "MemberExpression") ? resolveReactRefCurrentOriginSymbol(releaseCallee.object, context.scopes) : null;
|
|
15087
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed || !isNodeOfType(registrationCallee.property, "Identifier") || registrationCallee.property.name !== "addEventListener" || !isNodeOfType(releaseCallee, "MemberExpression") || releaseCallee.computed || !isNodeOfType(releaseCallee.property, "Identifier") || releaseCallee.property.name !== "removeEventListener" || !releaseRefSymbol) return false;
|
|
15088
|
+
const registrationReceiverKey = resolveExpressionKey(stripParenExpression(registrationCallee.object), context);
|
|
15089
|
+
const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
|
|
15090
|
+
const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
|
|
15091
|
+
if (registrationReceiverKey === null || registrationReceiverKey !== nodeParameterKey || releaseReceiverKey === null || usage.eventKey === null || usage.eventKey !== resolveExpressionKey(releaseCall.arguments?.[0], context) || usage.handlerKey === null || usage.handlerKey !== resolveExpressionKey(releaseCall.arguments?.[1], context)) return false;
|
|
15092
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15093
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15094
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15095
|
+
const releaseStart = getRangeStart(releaseCall);
|
|
15096
|
+
const matchingOwnershipAssignments = [];
|
|
15097
|
+
const usageFunctionBody = usageFunction.body;
|
|
15098
|
+
walkAst(usageFunctionBody, (child) => {
|
|
15099
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
15100
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.operator === "=" && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === releaseRefSymbol.id && resolveExpressionKey(child.right, context) === registrationReceiverKey && releaseStart !== null && (getRangeStart(child) ?? -1) > releaseStart) matchingOwnershipAssignments.push(child);
|
|
15101
|
+
});
|
|
15102
|
+
const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? releaseCall;
|
|
15103
|
+
const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
|
|
15104
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
15105
|
+
};
|
|
14893
15106
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
14894
15107
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14895
15108
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -14906,14 +15119,21 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14906
15119
|
if (!releaseVerbName) return false;
|
|
14907
15120
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
14908
15121
|
const releaseReceiverKey = resolveExpressionKey(callee.object, context);
|
|
15122
|
+
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
15123
|
+
const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
15124
|
+
const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
|
|
15125
|
+
const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
|
|
15126
|
+
const releaseReceiverCollectionKey = releaseReceiverForOfStatement ? resolveExpressionKey(releaseReceiverForOfStatement.right, context) : resolveIteratorCollectionKey(callee.object, context);
|
|
15127
|
+
if (pairedReleaseVerbNames && matchesPairedReleaseVerb(releaseVerbName, pairedReleaseVerbNames) && pushedResourceCollectionKey !== null && pushedResourceCollectionKey === releaseReceiverCollectionKey && (releaseVerbName !== "unobserve" || usage.eventKey !== null && releaseEventKey === usage.eventKey)) return true;
|
|
15128
|
+
if (isReactRefListenerReplacementRelease(callNode, usage, context)) return true;
|
|
14909
15129
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
14910
15130
|
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
14911
15131
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
14912
15132
|
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
14913
15133
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
15134
|
+
if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
14914
15135
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
14915
15136
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
14916
|
-
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
14917
15137
|
const usageEventArgument = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[0] : null;
|
|
14918
15138
|
const releaseEventArgument = callNode.arguments?.[0];
|
|
14919
15139
|
if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
|
|
@@ -14947,7 +15167,8 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14947
15167
|
const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
14948
15168
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
14949
15169
|
const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
|
|
14950
|
-
|
|
15170
|
+
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
|
|
15171
|
+
return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
|
|
14951
15172
|
}
|
|
14952
15173
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
14953
15174
|
return true;
|
|
@@ -14972,13 +15193,165 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
|
14972
15193
|
if (!symbol) return false;
|
|
14973
15194
|
return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
|
|
14974
15195
|
};
|
|
15196
|
+
const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
|
|
15197
|
+
if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
|
|
15198
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15199
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
|
|
15200
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
15201
|
+
const collectAssignment = (expression) => {
|
|
15202
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
15203
|
+
const assignment = expressionRoot.parent;
|
|
15204
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
|
|
15205
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
|
|
15206
|
+
const refCurrentKey = resolveExpressionKey(assignment.left, context);
|
|
15207
|
+
const retainedFunction = findEnclosingFunction$1(assignment);
|
|
15208
|
+
const assignmentStart = getRangeStart(assignment);
|
|
15209
|
+
if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
|
|
15210
|
+
assignments.set(assignmentStart, {
|
|
15211
|
+
assignmentNode: assignment,
|
|
15212
|
+
refCurrentKey,
|
|
15213
|
+
retainedFunction
|
|
15214
|
+
});
|
|
15215
|
+
};
|
|
15216
|
+
collectAssignment(disposerFunction);
|
|
15217
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
|
|
15218
|
+
const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
15219
|
+
for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
|
|
15220
|
+
walkAst(usageFunction.body, (child) => {
|
|
15221
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
15222
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
|
|
15223
|
+
});
|
|
15224
|
+
return [...assignments.values()];
|
|
15225
|
+
};
|
|
15226
|
+
const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
|
|
15227
|
+
const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
|
|
15228
|
+
let hasUnsafeOverwrite = false;
|
|
15229
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15230
|
+
if (hasUnsafeOverwrite) return false;
|
|
15231
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15232
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
|
|
15233
|
+
const storedValue = resolveStableValue(child.right, context);
|
|
15234
|
+
if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
|
|
15235
|
+
hasUnsafeOverwrite = true;
|
|
15236
|
+
return false;
|
|
15237
|
+
}
|
|
15238
|
+
});
|
|
15239
|
+
return hasUnsafeOverwrite;
|
|
15240
|
+
};
|
|
15241
|
+
const hasEffectCleanupInvocation = (storage, usage, context) => {
|
|
15242
|
+
const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
|
|
15243
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
|
|
15244
|
+
const cleanupFunctionInvokesRef = (cleanupFunction) => {
|
|
15245
|
+
if (!isFunctionLike$1(cleanupFunction)) return false;
|
|
15246
|
+
let didFindCleanupCall = false;
|
|
15247
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
15248
|
+
if (didFindCleanupCall) return false;
|
|
15249
|
+
if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15250
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
|
|
15251
|
+
const callRoot = findTransparentExpressionRoot(child);
|
|
15252
|
+
const callStatement = callRoot.parent;
|
|
15253
|
+
const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
|
|
15254
|
+
const isConciseBody = cleanupFunction.body === callRoot;
|
|
15255
|
+
if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
|
|
15256
|
+
didFindCleanupCall = true;
|
|
15257
|
+
return false;
|
|
15258
|
+
}
|
|
15259
|
+
}
|
|
15260
|
+
});
|
|
15261
|
+
return didFindCleanupCall;
|
|
15262
|
+
};
|
|
15263
|
+
const effectReturnsCleanup = (effectCallback) => {
|
|
15264
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
15265
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
15266
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
|
|
15267
|
+
return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
|
|
15268
|
+
}
|
|
15269
|
+
const matchingReturns = [];
|
|
15270
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15271
|
+
if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
15272
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
|
|
15273
|
+
if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
|
|
15274
|
+
matchingReturns.push(child);
|
|
15275
|
+
});
|
|
15276
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
15277
|
+
};
|
|
15278
|
+
let didFindInvocation = false;
|
|
15279
|
+
walkAst(componentFunction.body, (child) => {
|
|
15280
|
+
if (didFindInvocation) return false;
|
|
15281
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
|
|
15282
|
+
const effectCallback = getEffectCallback(child);
|
|
15283
|
+
if (effectCallback && effectReturnsCleanup(effectCallback)) {
|
|
15284
|
+
didFindInvocation = true;
|
|
15285
|
+
return false;
|
|
15286
|
+
}
|
|
15287
|
+
});
|
|
15288
|
+
return didFindInvocation;
|
|
15289
|
+
};
|
|
15290
|
+
const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
|
|
15291
|
+
const isReturnedCallbackRefShape = () => {
|
|
15292
|
+
if (!isFunctionLike$1(storage.retainedFunction)) return false;
|
|
15293
|
+
const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
|
|
15294
|
+
if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
|
|
15295
|
+
const nodeParameter = storage.retainedFunction.params?.[0];
|
|
15296
|
+
const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
|
|
15297
|
+
if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
|
|
15298
|
+
if (!isFunctionReturnedFromReactHook(storage.retainedFunction, context, false)) return false;
|
|
15299
|
+
const usageStart = getRangeStart(usage.node);
|
|
15300
|
+
if (usageStart === null) return false;
|
|
15301
|
+
let hasNullExit = false;
|
|
15302
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15303
|
+
if (hasNullExit) return false;
|
|
15304
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15305
|
+
if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
|
|
15306
|
+
const test = stripParenExpression(child.test);
|
|
15307
|
+
if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
|
|
15308
|
+
const consequent = child.consequent;
|
|
15309
|
+
hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
|
|
15310
|
+
if (hasNullExit) return false;
|
|
15311
|
+
});
|
|
15312
|
+
return hasNullExit;
|
|
15313
|
+
};
|
|
15314
|
+
if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
|
|
15315
|
+
const cleanupCalls = [];
|
|
15316
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15317
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15318
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
|
|
15319
|
+
});
|
|
15320
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
|
|
15321
|
+
};
|
|
15322
|
+
const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
|
|
15323
|
+
const disposerFunction = findEnclosingFunction$1(releaseNode);
|
|
15324
|
+
if (!disposerFunction) return false;
|
|
15325
|
+
return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
|
|
15326
|
+
};
|
|
15327
|
+
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
15328
|
+
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
|
|
15329
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15330
|
+
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
15331
|
+
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
15332
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15333
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15334
|
+
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
15335
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
15336
|
+
const triggerRegistrations = [];
|
|
15337
|
+
walkAst(ownerFunction.body, (child) => {
|
|
15338
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
15339
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15340
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15341
|
+
if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
|
|
15342
|
+
});
|
|
15343
|
+
if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
|
|
15344
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
|
|
15345
|
+
};
|
|
14975
15346
|
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
14976
15347
|
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
14977
15348
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
14978
15349
|
if (!releaseFunction) return true;
|
|
14979
15350
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
15351
|
+
if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
|
|
14980
15352
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14981
15353
|
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
15354
|
+
if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
|
|
14982
15355
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
14983
15356
|
};
|
|
14984
15357
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -17112,7 +17485,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
|
|
|
17112
17485
|
keys.add(depKey);
|
|
17113
17486
|
continue;
|
|
17114
17487
|
}
|
|
17115
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17488
|
+
const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17116
17489
|
if (identitySourceKeys) {
|
|
17117
17490
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
17118
17491
|
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
@@ -17195,6 +17568,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
|
17195
17568
|
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
17196
17569
|
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17197
17570
|
};
|
|
17571
|
+
const isPureDerivedExpression = (expression) => {
|
|
17572
|
+
const candidate = unwrapExpression$3(expression);
|
|
17573
|
+
if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
|
|
17574
|
+
if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
|
|
17575
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
|
|
17576
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
|
|
17577
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
|
|
17578
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
|
|
17579
|
+
return false;
|
|
17580
|
+
};
|
|
17581
|
+
const isPureDerivedStatement = (statement) => {
|
|
17582
|
+
if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
|
|
17583
|
+
if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
|
|
17584
|
+
if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
|
|
17585
|
+
return false;
|
|
17586
|
+
};
|
|
17587
|
+
const isPureDerivedFunction = (functionNode) => {
|
|
17588
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
|
|
17589
|
+
if (functionNode.async || functionNode.generator) return false;
|
|
17590
|
+
return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
|
|
17591
|
+
};
|
|
17592
|
+
const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
|
|
17593
|
+
if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
|
|
17594
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
17595
|
+
const callExpression = referenceRoot.parent;
|
|
17596
|
+
if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
|
|
17597
|
+
const functionNode = getFunctionValueNode(symbol);
|
|
17598
|
+
if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
|
|
17599
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17600
|
+
for (const capturedReference of closureCaptures(functionNode, scopes)) {
|
|
17601
|
+
const capturedSymbol = capturedReference.resolvedSymbol;
|
|
17602
|
+
if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
|
|
17603
|
+
if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
|
|
17604
|
+
const capturedKey = computeDepKey(capturedReference);
|
|
17605
|
+
if (!capturedKey) return null;
|
|
17606
|
+
if (capturedKey === capturedSymbol.name) {
|
|
17607
|
+
const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
|
|
17608
|
+
if (nestedSourceKeys) {
|
|
17609
|
+
for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
|
|
17610
|
+
continue;
|
|
17611
|
+
}
|
|
17612
|
+
}
|
|
17613
|
+
sourceKeys.add(capturedKey);
|
|
17614
|
+
}
|
|
17615
|
+
return sourceKeys.size > 0 ? sourceKeys : null;
|
|
17616
|
+
};
|
|
17617
|
+
const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
17618
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17619
|
+
for (const expression of expressions) {
|
|
17620
|
+
const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
|
|
17621
|
+
if (!expressionSourceKeys) return null;
|
|
17622
|
+
for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
|
|
17623
|
+
}
|
|
17624
|
+
return sourceKeys;
|
|
17625
|
+
};
|
|
17626
|
+
const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
|
|
17627
|
+
const candidate = unwrapExpression$3(expression);
|
|
17628
|
+
if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
|
|
17629
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
17630
|
+
if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
|
|
17631
|
+
const sourceSymbol = scopes.symbolFor(candidate);
|
|
17632
|
+
if (!sourceSymbol) return null;
|
|
17633
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17634
|
+
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
|
|
17635
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
17636
|
+
const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
17637
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
17638
|
+
if (sourceKeys) return sourceKeys;
|
|
17639
|
+
}
|
|
17640
|
+
return new Set([sourceSymbol.name]);
|
|
17641
|
+
}
|
|
17642
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
17643
|
+
if (hasComputedMemberExpression(candidate)) return null;
|
|
17644
|
+
const sourceKey = stringifyMemberChain(candidate);
|
|
17645
|
+
const rootIdentifier = getMemberRootIdentifier(candidate);
|
|
17646
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
17647
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
17648
|
+
if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17649
|
+
return new Set([sourceKey]);
|
|
17650
|
+
}
|
|
17651
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
|
|
17652
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
|
|
17653
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
|
|
17654
|
+
candidate.test,
|
|
17655
|
+
candidate.consequent,
|
|
17656
|
+
candidate.alternate
|
|
17657
|
+
], scopes, visitedSymbolIds);
|
|
17658
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
|
|
17659
|
+
if (isNodeOfType(candidate, "NewExpression")) {
|
|
17660
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
17661
|
+
if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
|
|
17662
|
+
const argumentsToAnalyze = [];
|
|
17663
|
+
for (const argument of candidate.arguments) {
|
|
17664
|
+
if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
|
|
17665
|
+
argumentsToAnalyze.push(argument);
|
|
17666
|
+
}
|
|
17667
|
+
return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
|
|
17668
|
+
}
|
|
17669
|
+
return null;
|
|
17670
|
+
};
|
|
17671
|
+
const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
|
|
17672
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17673
|
+
let currentNode = assignment;
|
|
17674
|
+
while (currentNode.parent && currentNode.parent !== boundaryFunction) {
|
|
17675
|
+
const parentNode = currentNode.parent;
|
|
17676
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
17677
|
+
if (parentNode.test === currentNode) return null;
|
|
17678
|
+
const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
|
|
17679
|
+
if (!testSourceKeys) return null;
|
|
17680
|
+
for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
|
|
17681
|
+
} else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
|
|
17682
|
+
currentNode = parentNode;
|
|
17683
|
+
}
|
|
17684
|
+
return currentNode.parent === boundaryFunction ? sourceKeys : null;
|
|
17685
|
+
};
|
|
17686
|
+
const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
|
|
17687
|
+
const referenceRoot = findTransparentExpressionRoot(referenceNode);
|
|
17688
|
+
const callExpression = referenceRoot.parent;
|
|
17689
|
+
return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
|
|
17690
|
+
allowGlobalReactNamespace: true,
|
|
17691
|
+
allowUnboundBareCalls: true,
|
|
17692
|
+
resolveNamedAliases: true
|
|
17693
|
+
});
|
|
17694
|
+
};
|
|
17695
|
+
const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
|
|
17696
|
+
if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
17697
|
+
const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
17698
|
+
if (!boundaryFunction) return null;
|
|
17699
|
+
const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
|
|
17700
|
+
if (!capturingFunction || capturingFunction === boundaryFunction) return null;
|
|
17701
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17702
|
+
if (symbol.initializer) {
|
|
17703
|
+
const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17704
|
+
if (!initializerSourceKeys) return null;
|
|
17705
|
+
for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
|
|
17706
|
+
}
|
|
17707
|
+
let writeCount = 0;
|
|
17708
|
+
for (const symbolReference of symbol.references) {
|
|
17709
|
+
if (symbolReference.flag === "read") {
|
|
17710
|
+
if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
|
|
17711
|
+
continue;
|
|
17712
|
+
}
|
|
17713
|
+
if (symbolReference.flag !== "write") return null;
|
|
17714
|
+
const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
|
|
17715
|
+
const assignment = referenceRoot.parent;
|
|
17716
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
|
|
17717
|
+
const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
|
|
17718
|
+
const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
|
|
17719
|
+
if (!assignmentSourceKeys || !controlSourceKeys) return null;
|
|
17720
|
+
for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
|
|
17721
|
+
for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
|
|
17722
|
+
writeCount += 1;
|
|
17723
|
+
}
|
|
17724
|
+
return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
|
|
17725
|
+
};
|
|
17198
17726
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
17199
17727
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
17200
17728
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
@@ -30212,6 +30740,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
|
|
|
30212
30740
|
const importDeclaration = declarationNode.parent;
|
|
30213
30741
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
30214
30742
|
}));
|
|
30743
|
+
const isReactNamespaceReceiver = (analysis, node) => {
|
|
30744
|
+
const receiver = stripParenExpression(node);
|
|
30745
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30746
|
+
const namespaceReference = getRef(analysis, receiver);
|
|
30747
|
+
return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
|
|
30748
|
+
};
|
|
30215
30749
|
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
30216
30750
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
30217
30751
|
const callee = stripParenExpression(declarator.init.callee);
|
|
@@ -30220,24 +30754,20 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
|
30220
30754
|
if (!reference?.resolved) return callee.name === hookName;
|
|
30221
30755
|
return isReactNamedImportReference(reference, hookName);
|
|
30222
30756
|
}
|
|
30223
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.
|
|
30224
|
-
|
|
30225
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
30226
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
30757
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
30758
|
+
return isReactNamespaceReceiver(analysis, callee.object);
|
|
30227
30759
|
};
|
|
30228
30760
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
30229
30761
|
if (!node) return false;
|
|
30230
30762
|
if (isNodeOfType(node, "Identifier")) {
|
|
30231
30763
|
if (node.name === hookName) return true;
|
|
30232
30764
|
if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
|
|
30233
|
-
const
|
|
30234
|
-
|
|
30765
|
+
const receiverRoot = findTransparentExpressionRoot(node);
|
|
30766
|
+
const parent = receiverRoot.parent;
|
|
30767
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
30235
30768
|
return false;
|
|
30236
30769
|
}
|
|
30237
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
30238
|
-
const receiver = stripParenExpression(node.object);
|
|
30239
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30240
|
-
}
|
|
30770
|
+
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30241
30771
|
return false;
|
|
30242
30772
|
};
|
|
30243
30773
|
const isUseEffect = (node) => {
|
|
@@ -30345,7 +30875,27 @@ const isRefCurrent = (ref) => {
|
|
|
30345
30875
|
if (!isNodeOfType(parent.property, "Identifier")) return false;
|
|
30346
30876
|
return parent.property.name === "current";
|
|
30347
30877
|
};
|
|
30348
|
-
const
|
|
30878
|
+
const resolveStateSetterReference = (analysis, ref) => {
|
|
30879
|
+
const visitedReferences = /* @__PURE__ */ new Set();
|
|
30880
|
+
let currentReference = ref;
|
|
30881
|
+
while (currentReference && !visitedReferences.has(currentReference)) {
|
|
30882
|
+
if (isStateSetter(analysis, currentReference)) return currentReference;
|
|
30883
|
+
visitedReferences.add(currentReference);
|
|
30884
|
+
const definitions = currentReference.resolved?.defs ?? [];
|
|
30885
|
+
if (definitions.length !== 1) return null;
|
|
30886
|
+
const definitionNode = definitions[0].node;
|
|
30887
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator")) return null;
|
|
30888
|
+
if (!isNodeOfType(definitionNode.id, "Identifier")) return null;
|
|
30889
|
+
const declaration = definitionNode.parent;
|
|
30890
|
+
if (!isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30891
|
+
if (!definitionNode.init) return null;
|
|
30892
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
30893
|
+
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
30894
|
+
currentReference = getRef(analysis, initializer);
|
|
30895
|
+
}
|
|
30896
|
+
return null;
|
|
30897
|
+
};
|
|
30898
|
+
const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => resolveStateSetterReference(analysis, innerRef) !== null);
|
|
30349
30899
|
const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
|
|
30350
30900
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
30351
30901
|
const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
|
|
@@ -30496,9 +31046,11 @@ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
|
|
|
30496
31046
|
};
|
|
30497
31047
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
30498
31048
|
const getUseStateDecl = (analysis, ref) => {
|
|
30499
|
-
|
|
30500
|
-
|
|
30501
|
-
|
|
31049
|
+
const definition = getUpstreamRefs(analysis, ref).find((upstreamReference) => isState(analysis, upstreamReference) || isStateSetter(analysis, upstreamReference))?.resolved?.defs.find((candidateDefinition) => {
|
|
31050
|
+
const definitionNode = candidateDefinition.node;
|
|
31051
|
+
return isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && isHookCallee$1(analysis, definitionNode.init.callee, "useState");
|
|
31052
|
+
});
|
|
31053
|
+
return definition ? definition.node : null;
|
|
30502
31054
|
};
|
|
30503
31055
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
30504
31056
|
if (isFunctionLike$1(node)) return true;
|
|
@@ -30661,7 +31213,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
|
|
|
30661
31213
|
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
30662
31214
|
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
30663
31215
|
};
|
|
30664
|
-
const
|
|
31216
|
+
const isSynchronousFunction = (functionNode) => {
|
|
31217
|
+
const functionMetadata = functionNode;
|
|
31218
|
+
return functionMetadata.async !== true && functionMetadata.generator !== true;
|
|
31219
|
+
};
|
|
31220
|
+
const findBindingVariable = (analysis, bindingIdentifier) => {
|
|
31221
|
+
for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
|
|
31222
|
+
return null;
|
|
31223
|
+
};
|
|
31224
|
+
const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
|
|
31225
|
+
if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
|
|
31226
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
31227
|
+
if (!bindingIdentifier) return null;
|
|
31228
|
+
const variable = findBindingVariable(analysis, bindingIdentifier);
|
|
31229
|
+
if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
31230
|
+
const definition = variable.defs[0];
|
|
31231
|
+
if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
|
|
31232
|
+
if (definition.type !== "Variable") return null;
|
|
31233
|
+
const declarator = definition.node;
|
|
31234
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
|
|
31235
|
+
if (declarator.init === functionNode) return variable;
|
|
31236
|
+
if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
|
|
31237
|
+
return null;
|
|
31238
|
+
};
|
|
31239
|
+
const getJsxEventValueAttribute = (identifier) => {
|
|
31240
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31241
|
+
const expressionContainer = expression.parent;
|
|
31242
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
|
|
31243
|
+
const attribute = expressionContainer.parent;
|
|
31244
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) return null;
|
|
31245
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
31246
|
+
return attributeName && isEventHandlerName(attributeName) ? attribute : null;
|
|
31247
|
+
};
|
|
31248
|
+
const getInlineJsxEventCallbackAttribute = (callExpression) => {
|
|
31249
|
+
const callbackFunction = findEnclosingFunction$1(callExpression);
|
|
31250
|
+
if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
|
|
31251
|
+
return getJsxEventValueAttribute(callbackFunction);
|
|
31252
|
+
};
|
|
31253
|
+
const isReactHookDependencyReference = (identifier) => {
|
|
31254
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31255
|
+
const dependencyArray = expression.parent;
|
|
31256
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
|
|
31257
|
+
const hookCall = dependencyArray.parent;
|
|
31258
|
+
if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
|
|
31259
|
+
const callee = hookCall.callee;
|
|
31260
|
+
if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
|
|
31261
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
|
|
31262
|
+
};
|
|
31263
|
+
const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
|
|
31264
|
+
if (visitedVariables.has(functionVariable)) return false;
|
|
31265
|
+
const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
|
|
31266
|
+
const callExpressions = [];
|
|
31267
|
+
let hasDirectJsxEventReference = false;
|
|
31268
|
+
for (const reference of functionVariable.references) {
|
|
31269
|
+
if (reference.init) continue;
|
|
31270
|
+
const identifier = reference.identifier;
|
|
31271
|
+
if (reference.isWrite()) return false;
|
|
31272
|
+
const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
|
|
31273
|
+
if (jsxEventValueAttribute) {
|
|
31274
|
+
if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
|
|
31275
|
+
continue;
|
|
31276
|
+
}
|
|
31277
|
+
if (isReactHookDependencyReference(identifier)) continue;
|
|
31278
|
+
const callExpression = getCallExpr(reference);
|
|
31279
|
+
if (!callExpression) return false;
|
|
31280
|
+
const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
|
|
31281
|
+
if (jsxEventCallbackAttribute) {
|
|
31282
|
+
if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
|
|
31283
|
+
continue;
|
|
31284
|
+
}
|
|
31285
|
+
callExpressions.push(callExpression);
|
|
31286
|
+
}
|
|
31287
|
+
if (hasDirectJsxEventReference) return true;
|
|
31288
|
+
for (const callExpression of callExpressions) {
|
|
31289
|
+
if (!isNodeReachableWithinFunction(callExpression, context)) continue;
|
|
31290
|
+
const callerFunction = findEnclosingFunction$1(callExpression);
|
|
31291
|
+
if (!callerFunction || callerFunction === componentFunction) continue;
|
|
31292
|
+
const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
|
|
31293
|
+
if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
|
|
31294
|
+
}
|
|
31295
|
+
return false;
|
|
31296
|
+
};
|
|
31297
|
+
const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
|
|
30665
31298
|
if (!setterRef.resolved) return false;
|
|
30666
31299
|
const componentFunction = findEnclosingFunction$1(effectNode);
|
|
30667
31300
|
if (!componentFunction) return false;
|
|
@@ -30670,6 +31303,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
|
|
|
30670
31303
|
const identifier = reference.identifier;
|
|
30671
31304
|
if (isAstDescendant(identifier, effectNode)) continue;
|
|
30672
31305
|
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
31306
|
+
if (!isNodeReachableWithinFunction(identifier, context)) continue;
|
|
31307
|
+
const writerFunction = findEnclosingFunction$1(identifier);
|
|
31308
|
+
if (!writerFunction || writerFunction === componentFunction) continue;
|
|
31309
|
+
const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
|
|
31310
|
+
if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
|
|
30673
31311
|
}
|
|
30674
31312
|
return false;
|
|
30675
31313
|
};
|
|
@@ -31559,7 +32197,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
31559
32197
|
}
|
|
31560
32198
|
return false;
|
|
31561
32199
|
};
|
|
31562
|
-
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
32200
|
+
const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
|
|
31563
32201
|
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
31564
32202
|
if (frames.length === 0) return [];
|
|
31565
32203
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
@@ -31589,7 +32227,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31589
32227
|
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
31590
32228
|
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
31591
32229
|
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
31592
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
32230
|
+
const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
|
|
31593
32231
|
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
31594
32232
|
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
31595
32233
|
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
@@ -31624,13 +32262,18 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
31624
32262
|
tags: ["test-noise"],
|
|
31625
32263
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
31626
32264
|
create: (context) => ({ CallExpression(node) {
|
|
31627
|
-
if (!
|
|
32265
|
+
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
32266
|
+
allowGlobalReactNamespace: true,
|
|
32267
|
+
allowUnboundBareCalls: true,
|
|
32268
|
+
resolveConditionalAliases: true,
|
|
32269
|
+
resolveNamedAliases: true
|
|
32270
|
+
})) return;
|
|
31628
32271
|
const analysis = getProgramAnalysis(node);
|
|
31629
32272
|
if (!analysis) return;
|
|
31630
32273
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31631
32274
|
if (!dependencyReferences) return;
|
|
31632
32275
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31633
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
32276
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
31634
32277
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
31635
32278
|
context.report({
|
|
31636
32279
|
node: fact.callExpression,
|
|
@@ -34351,7 +34994,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34351
34994
|
if (!callExpr) continue;
|
|
34352
34995
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34353
34996
|
if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
|
|
34354
|
-
const declarator = getUseStateDeclarator(ref);
|
|
34997
|
+
const declarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34355
34998
|
if (declarator) domSyncedStateDeclarators.add(declarator);
|
|
34356
34999
|
}
|
|
34357
35000
|
for (const ref of effectFnRefs) {
|
|
@@ -34360,7 +35003,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34360
35003
|
if (!callExpr) continue;
|
|
34361
35004
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34362
35005
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
|
|
34363
|
-
const setterDeclarator = getUseStateDeclarator(ref);
|
|
35006
|
+
const setterDeclarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34364
35007
|
if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
|
|
34365
35008
|
const isSelfTargeting = setterDeclarator !== null && stateDepDeclarators.has(setterDeclarator);
|
|
34366
35009
|
const setterArguments = isNodeOfType(callExpr, "CallExpression") ? callExpr.arguments ?? [] : [];
|
|
@@ -36201,10 +36844,15 @@ const noDerivedState = defineRule({
|
|
|
36201
36844
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36202
36845
|
} }).visitors,
|
|
36203
36846
|
CallExpression(node) {
|
|
36204
|
-
if (!
|
|
36847
|
+
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
36848
|
+
allowGlobalReactNamespace: true,
|
|
36849
|
+
allowUnboundBareCalls: true,
|
|
36850
|
+
resolveConditionalAliases: true,
|
|
36851
|
+
resolveNamedAliases: true
|
|
36852
|
+
})) return;
|
|
36205
36853
|
const analysis = getProgramAnalysis(node);
|
|
36206
36854
|
if (!analysis) return;
|
|
36207
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
36855
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
36208
36856
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
36209
36857
|
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36210
36858
|
}
|
|
@@ -36221,10 +36869,15 @@ const noDerivedStateEffect = defineRule({
|
|
|
36221
36869
|
tags: ["test-noise"],
|
|
36222
36870
|
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
36223
36871
|
create: (context) => ({ CallExpression(node) {
|
|
36224
|
-
if (!
|
|
36872
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
36873
|
+
allowGlobalReactNamespace: true,
|
|
36874
|
+
allowUnboundBareCalls: true,
|
|
36875
|
+
resolveConditionalAliases: true,
|
|
36876
|
+
resolveNamedAliases: true
|
|
36877
|
+
})) return;
|
|
36225
36878
|
const analysis = getProgramAnalysis(node);
|
|
36226
36879
|
if (!analysis) return;
|
|
36227
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36880
|
+
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36228
36881
|
context.report({
|
|
36229
36882
|
node,
|
|
36230
36883
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -37173,7 +37826,7 @@ const noDirectMutationState = defineRule({
|
|
|
37173
37826
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
37174
37827
|
//#endregion
|
|
37175
37828
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
37176
|
-
const collectUseStateBindings = (componentBody) => {
|
|
37829
|
+
const collectUseStateBindings = (componentBody, scopes) => {
|
|
37177
37830
|
const bindings = [];
|
|
37178
37831
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
37179
37832
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -37186,7 +37839,12 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
37186
37839
|
const setterElement = elements[1];
|
|
37187
37840
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
37188
37841
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
37189
|
-
if (!
|
|
37842
|
+
if (!(scopes ? isReactApiCall(declarator.init, "useState", scopes, {
|
|
37843
|
+
allowGlobalReactNamespace: true,
|
|
37844
|
+
allowUnboundBareCalls: true,
|
|
37845
|
+
resolveConditionalAliases: true,
|
|
37846
|
+
resolveNamedAliases: true
|
|
37847
|
+
}) : isHookCall$2(declarator.init, "useState"))) continue;
|
|
37190
37848
|
bindings.push({
|
|
37191
37849
|
valueName: valueElement.name,
|
|
37192
37850
|
setterName: setterElement.name,
|
|
@@ -37913,25 +38571,36 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
37913
38571
|
};
|
|
37914
38572
|
//#endregion
|
|
37915
38573
|
//#region src/plugin/rules/state-and-effects/no-effect-chain.ts
|
|
37916
|
-
const findTopLevelEffectCalls = (componentBody) => {
|
|
38574
|
+
const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
37917
38575
|
const effectCalls = [];
|
|
37918
38576
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
37919
38577
|
for (const statement of componentBody.body ?? []) {
|
|
37920
38578
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37921
38579
|
const expression = unwrapDiscardedExpression(statement);
|
|
37922
38580
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
37923
|
-
if (!
|
|
38581
|
+
if (!isReactApiCall(expression, EFFECT_HOOK_NAMES$1, scopes, {
|
|
38582
|
+
allowGlobalReactNamespace: true,
|
|
38583
|
+
allowUnboundBareCalls: true,
|
|
38584
|
+
resolveConditionalAliases: true,
|
|
38585
|
+
resolveNamedAliases: true
|
|
38586
|
+
})) continue;
|
|
37924
38587
|
effectCalls.push(expression);
|
|
37925
38588
|
}
|
|
37926
38589
|
return effectCalls;
|
|
37927
38590
|
};
|
|
37928
|
-
const
|
|
37929
|
-
const
|
|
37930
|
-
if (!isNodeOfType(effectNode, "CallExpression")) return
|
|
38591
|
+
const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) => {
|
|
38592
|
+
const dependencyStateSymbolIds = /* @__PURE__ */ new Set();
|
|
38593
|
+
if (!isNodeOfType(effectNode, "CallExpression")) return dependencyStateSymbolIds;
|
|
37931
38594
|
const depsNode = effectNode.arguments?.[1];
|
|
37932
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) return
|
|
37933
|
-
for (const element of depsNode.elements ?? [])
|
|
37934
|
-
|
|
38595
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyStateSymbolIds;
|
|
38596
|
+
for (const element of depsNode.elements ?? []) {
|
|
38597
|
+
if (!element || isNodeOfType(element, "SpreadElement")) continue;
|
|
38598
|
+
const rootIdentifier = getRootIdentifier$1(element);
|
|
38599
|
+
if (!isNodeOfType(rootIdentifier, "Identifier")) continue;
|
|
38600
|
+
const symbol = resolveConstIdentifierAlias(rootIdentifier, scopes, true);
|
|
38601
|
+
if (symbol && stateSymbolIds.has(symbol.id)) dependencyStateSymbolIds.add(symbol.id);
|
|
38602
|
+
}
|
|
38603
|
+
return dependencyStateSymbolIds;
|
|
37935
38604
|
};
|
|
37936
38605
|
const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
|
|
37937
38606
|
const analysisFunctions = new Set([effectCallback]);
|
|
@@ -38037,12 +38706,13 @@ const readStaticSetterValue = (setterCall, scopes) => {
|
|
|
38037
38706
|
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
38038
38707
|
return readStaticEffectValue(argument, scopes, null, null);
|
|
38039
38708
|
};
|
|
38040
|
-
const collectStateWritesInEffect = (analysisFunctions,
|
|
38709
|
+
const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName, scopes) => {
|
|
38041
38710
|
const stateWrites = /* @__PURE__ */ new Map();
|
|
38042
38711
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38043
38712
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
38044
38713
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
38045
|
-
const
|
|
38714
|
+
const setterSymbol = resolveConstIdentifierAlias(child.callee, scopes, true);
|
|
38715
|
+
const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
|
|
38046
38716
|
if (!stateName) return;
|
|
38047
38717
|
const writeInfo = stateWrites.get(stateName) ?? {
|
|
38048
38718
|
values: /* @__PURE__ */ new Set(),
|
|
@@ -38128,11 +38798,12 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
|
38128
38798
|
"keys",
|
|
38129
38799
|
"values"
|
|
38130
38800
|
]);
|
|
38131
|
-
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
38801
|
+
const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
|
|
38132
38802
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
38133
38803
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
38134
38804
|
if (isNodeOfType(returnedValue.callee, "Identifier")) {
|
|
38135
|
-
|
|
38805
|
+
const setterSymbol = resolveConstIdentifierAlias(returnedValue.callee, scopes, true);
|
|
38806
|
+
if (setterToStateName.has(returnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
|
|
38136
38807
|
if (isSetterIdentifier(returnedValue.callee.name)) return true;
|
|
38137
38808
|
}
|
|
38138
38809
|
return isCleanupReturn(returnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
|
|
@@ -38299,11 +38970,11 @@ const isExternalSyncNode = (node) => {
|
|
|
38299
38970
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
38300
38971
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
38301
38972
|
};
|
|
38302
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
38973
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
|
|
38303
38974
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
38304
38975
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
38305
|
-
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
38306
|
-
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
|
|
38976
|
+
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, setterSymbolIdToStateName, scopes, false)) return true;
|
|
38977
|
+
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, setterSymbolIdToStateName, scopes, true)) return true;
|
|
38307
38978
|
let didFindExternalCall = false;
|
|
38308
38979
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38309
38980
|
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
@@ -38319,10 +38990,11 @@ const noEffectChain = defineRule({
|
|
|
38319
38990
|
create: (context) => {
|
|
38320
38991
|
const checkComponent = (componentBody) => {
|
|
38321
38992
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38322
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
38993
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
38323
38994
|
if (useStateBindings.length === 0) return;
|
|
38324
38995
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
38325
38996
|
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
38997
|
+
const setterSymbolIdToStateName = /* @__PURE__ */ new Map();
|
|
38326
38998
|
for (const binding of useStateBindings) {
|
|
38327
38999
|
setterToStateName.set(binding.setterName, binding.valueName);
|
|
38328
39000
|
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
@@ -38331,21 +39003,27 @@ const noEffectChain = defineRule({
|
|
|
38331
39003
|
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
38332
39004
|
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
38333
39005
|
}
|
|
39006
|
+
const setterIdentifier = binding.declarator.id.elements[1];
|
|
39007
|
+
if (isNodeOfType(setterIdentifier, "Identifier")) {
|
|
39008
|
+
const setterSymbol = context.scopes.symbolFor(setterIdentifier);
|
|
39009
|
+
if (setterSymbol) setterSymbolIdToStateName.set(setterSymbol.id, binding.valueName);
|
|
39010
|
+
}
|
|
38334
39011
|
}
|
|
38335
39012
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
39013
|
+
const stateSymbolIdSet = new Set(stateSymbolIds.values());
|
|
38336
39014
|
const effectInfos = [];
|
|
38337
|
-
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
39015
|
+
for (const effectCall of findTopLevelEffectCalls(componentBody, context.scopes)) {
|
|
38338
39016
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
38339
39017
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
38340
39018
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
38341
|
-
const stateWrites = collectStateWritesInEffect(analysisFunctions,
|
|
39019
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterSymbolIdToStateName, context.scopes);
|
|
38342
39020
|
const writtenStateNames = new Set(stateWrites.keys());
|
|
38343
39021
|
effectInfos.push({
|
|
38344
39022
|
node: effectCall,
|
|
38345
|
-
|
|
39023
|
+
dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
|
|
38346
39024
|
stateWrites,
|
|
38347
39025
|
analysisFunctions,
|
|
38348
|
-
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
39026
|
+
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
38349
39027
|
});
|
|
38350
39028
|
}
|
|
38351
39029
|
if (effectInfos.length < 2) return;
|
|
@@ -38356,10 +39034,11 @@ const noEffectChain = defineRule({
|
|
|
38356
39034
|
for (const readerEffect of effectInfos) {
|
|
38357
39035
|
if (readerEffect === writerEffect) continue;
|
|
38358
39036
|
if (readerEffect.isExternalSync) continue;
|
|
38359
|
-
if (readerEffect.
|
|
39037
|
+
if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
|
|
38360
39038
|
let chainedStateName = null;
|
|
38361
39039
|
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
38362
|
-
|
|
39040
|
+
const writtenStateSymbolId = stateSymbolIds.get(writtenName);
|
|
39041
|
+
if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
|
|
38363
39042
|
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
38364
39043
|
chainedStateName = writtenName;
|
|
38365
39044
|
break;
|
|
@@ -40175,6 +40854,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
|
|
|
40175
40854
|
"scrollHeight"
|
|
40176
40855
|
]);
|
|
40177
40856
|
const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
|
|
40857
|
+
const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
|
|
40858
|
+
"blur",
|
|
40859
|
+
"focus",
|
|
40860
|
+
"restoreSelection",
|
|
40861
|
+
"scroll",
|
|
40862
|
+
"scrollBy",
|
|
40863
|
+
"scrollIntoView",
|
|
40864
|
+
"scrollTo",
|
|
40865
|
+
"setRangeText",
|
|
40866
|
+
"setSelectionRange"
|
|
40867
|
+
]);
|
|
40178
40868
|
const subtreeReadsDomMeasurement = (root) => {
|
|
40179
40869
|
if (!root) return false;
|
|
40180
40870
|
let found = false;
|
|
@@ -40193,29 +40883,66 @@ const subtreeReadsDomMeasurement = (root) => {
|
|
|
40193
40883
|
});
|
|
40194
40884
|
return found;
|
|
40195
40885
|
};
|
|
40196
|
-
const
|
|
40886
|
+
const collectFunctionNamesMatchingBody = (program, matchesBody) => {
|
|
40197
40887
|
const names = /* @__PURE__ */ new Set();
|
|
40198
40888
|
walkAst(program, (child) => {
|
|
40199
40889
|
if (isNodeOfType(child, "FunctionDeclaration")) {
|
|
40200
|
-
if (child.id && isNodeOfType(child.id, "Identifier") &&
|
|
40890
|
+
if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
|
|
40201
40891
|
return;
|
|
40202
40892
|
}
|
|
40203
40893
|
if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
|
|
40204
40894
|
let functionValue = child.init;
|
|
40205
40895
|
if (functionValue && isNodeOfType(functionValue, "CallExpression") && isNodeOfType(functionValue.callee, "Identifier") && /^use[A-Z]/.test(functionValue.callee.name)) functionValue = functionValue.arguments?.[0];
|
|
40206
|
-
if (functionValue && isFunctionLike$1(functionValue) &&
|
|
40896
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
40207
40897
|
});
|
|
40208
40898
|
return names;
|
|
40209
40899
|
};
|
|
40210
|
-
const
|
|
40211
|
-
|
|
40900
|
+
const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
|
|
40901
|
+
const subtreeMutatesDomImperatively = (root) => {
|
|
40902
|
+
if (!root || isFunctionLike$1(root)) return false;
|
|
40903
|
+
let found = false;
|
|
40904
|
+
walkAst(root, (child) => {
|
|
40905
|
+
if (found) return false;
|
|
40906
|
+
if (child !== root && isFunctionLike$1(child)) return false;
|
|
40907
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40908
|
+
const callee = stripParenExpression(child.callee);
|
|
40909
|
+
const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
40910
|
+
if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
|
|
40911
|
+
found = true;
|
|
40912
|
+
return false;
|
|
40913
|
+
}
|
|
40914
|
+
});
|
|
40915
|
+
return found;
|
|
40916
|
+
};
|
|
40917
|
+
const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
|
|
40918
|
+
const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
|
|
40919
|
+
if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
|
|
40212
40920
|
let found = false;
|
|
40213
40921
|
walkAst(root, (child) => {
|
|
40214
40922
|
if (found) return false;
|
|
40923
|
+
if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
|
|
40215
40924
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
|
|
40216
40925
|
});
|
|
40217
40926
|
return found;
|
|
40218
40927
|
};
|
|
40928
|
+
const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
|
|
40929
|
+
let statement = call;
|
|
40930
|
+
let parent = statement.parent;
|
|
40931
|
+
while (parent) {
|
|
40932
|
+
const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
|
|
40933
|
+
if (statements) {
|
|
40934
|
+
const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
|
|
40935
|
+
if (statementIndex >= 0) {
|
|
40936
|
+
const nextStatement = statements[statementIndex + 1];
|
|
40937
|
+
return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
|
|
40938
|
+
}
|
|
40939
|
+
}
|
|
40940
|
+
if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
40941
|
+
statement = parent;
|
|
40942
|
+
parent = parent.parent;
|
|
40943
|
+
}
|
|
40944
|
+
return false;
|
|
40945
|
+
};
|
|
40219
40946
|
const isInsideStartViewTransition = (node) => {
|
|
40220
40947
|
let cursor = node.parent;
|
|
40221
40948
|
while (cursor) {
|
|
@@ -40256,11 +40983,12 @@ const importsImperativeDomLibrary = (program) => {
|
|
|
40256
40983
|
};
|
|
40257
40984
|
const hasExemptFlushSyncCall = (program, localName) => {
|
|
40258
40985
|
const measuringFunctionNames = collectMeasuringFunctionNames(program);
|
|
40986
|
+
const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
|
|
40259
40987
|
let exempt = false;
|
|
40260
40988
|
walkAst(program, (child) => {
|
|
40261
40989
|
if (exempt) return false;
|
|
40262
40990
|
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
|
|
40263
|
-
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
|
|
40991
|
+
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
|
|
40264
40992
|
exempt = true;
|
|
40265
40993
|
return false;
|
|
40266
40994
|
}
|
|
@@ -40825,41 +41553,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
|
40825
41553
|
if (leftResult === false && rightResult === false) return false;
|
|
40826
41554
|
return null;
|
|
40827
41555
|
};
|
|
40828
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
41556
|
+
const readHydrationConditionResult = (expression, context, runtime, state) => {
|
|
40829
41557
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40830
41558
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40831
41559
|
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
40832
41560
|
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
40833
41561
|
if (staticResult !== null) return staticResult;
|
|
41562
|
+
const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
|
|
41563
|
+
const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
|
|
41564
|
+
if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41565
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41566
|
+
const result = readHydrationConditionResult(parameterValue, context, runtime, state);
|
|
41567
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41568
|
+
return result;
|
|
41569
|
+
}
|
|
41570
|
+
if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41571
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41572
|
+
const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
|
|
41573
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41574
|
+
return result;
|
|
41575
|
+
}
|
|
41576
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41577
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41578
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41579
|
+
allowGlobalReactNamespace: true,
|
|
41580
|
+
resolveNamedAliases: true
|
|
41581
|
+
})) {
|
|
41582
|
+
const callbackArgument = callArguments[0];
|
|
41583
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41584
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41585
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
|
|
41586
|
+
}
|
|
41587
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41588
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
|
|
41589
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41590
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41591
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41592
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41593
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41594
|
+
const argument = callArguments[parameterIndex];
|
|
41595
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41596
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41597
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41598
|
+
}
|
|
41599
|
+
return readHydrationFunctionResult(helperFunction, context, runtime, {
|
|
41600
|
+
...state,
|
|
41601
|
+
parameterValuesBySymbolId
|
|
41602
|
+
});
|
|
41603
|
+
}
|
|
40834
41604
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
40835
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
41605
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
|
|
40836
41606
|
return argumentResult === null ? null : !argumentResult;
|
|
40837
41607
|
}
|
|
40838
41608
|
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
40839
|
-
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
41609
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
|
|
41610
|
+
};
|
|
41611
|
+
const readHydrationStatementResult = (statement, context, runtime, state) => {
|
|
41612
|
+
if (isNodeOfType(statement, "ReturnStatement")) return {
|
|
41613
|
+
didReturn: true,
|
|
41614
|
+
value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
|
|
41615
|
+
};
|
|
41616
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
41617
|
+
for (const childStatement of statement.body) {
|
|
41618
|
+
const result = readHydrationStatementResult(childStatement, context, runtime, state);
|
|
41619
|
+
if (result.didReturn) return result;
|
|
41620
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41621
|
+
}
|
|
41622
|
+
return {
|
|
41623
|
+
didReturn: false,
|
|
41624
|
+
value: null
|
|
41625
|
+
};
|
|
41626
|
+
}
|
|
41627
|
+
if (!isNodeOfType(statement, "IfStatement")) return {
|
|
41628
|
+
didReturn: false,
|
|
41629
|
+
value: null
|
|
41630
|
+
};
|
|
41631
|
+
const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
|
|
41632
|
+
if (conditionResult !== null) {
|
|
41633
|
+
const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
|
|
41634
|
+
return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
|
|
41635
|
+
didReturn: false,
|
|
41636
|
+
value: null
|
|
41637
|
+
};
|
|
41638
|
+
}
|
|
41639
|
+
const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
|
|
41640
|
+
const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
|
|
41641
|
+
didReturn: false,
|
|
41642
|
+
value: null
|
|
41643
|
+
};
|
|
41644
|
+
return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
|
|
41645
|
+
didReturn: consequentResult.didReturn || alternateResult.didReturn,
|
|
41646
|
+
value: null
|
|
41647
|
+
};
|
|
41648
|
+
};
|
|
41649
|
+
const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
|
|
41650
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41651
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41652
|
+
const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
|
|
41653
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41654
|
+
return result;
|
|
41655
|
+
};
|
|
41656
|
+
const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
|
|
41657
|
+
const left = stripParenExpression(leftExpression);
|
|
41658
|
+
const right = stripParenExpression(rightExpression);
|
|
41659
|
+
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
|
|
41660
|
+
const leftSymbol = scopes.symbolFor(left);
|
|
41661
|
+
const rightSymbol = scopes.symbolFor(right);
|
|
41662
|
+
return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
|
|
41663
|
+
}
|
|
41664
|
+
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
|
|
41665
|
+
if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
|
|
41666
|
+
const rightArguments = right.arguments ?? [];
|
|
41667
|
+
return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
|
|
41668
|
+
const rightArgument = rightArguments[index];
|
|
41669
|
+
return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
|
|
41670
|
+
});
|
|
41671
|
+
}
|
|
41672
|
+
return true;
|
|
41673
|
+
};
|
|
41674
|
+
const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
|
|
41675
|
+
if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
|
|
41676
|
+
const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
|
|
41677
|
+
const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
|
|
41678
|
+
return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
|
|
41679
|
+
};
|
|
41680
|
+
const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
|
|
41681
|
+
const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
|
|
41682
|
+
return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
|
|
40840
41683
|
};
|
|
40841
|
-
const
|
|
41684
|
+
const matchHydrationConditionInternal = (expression, context, state) => {
|
|
40842
41685
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40843
41686
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40844
41687
|
if (predicateMatch) return {
|
|
40845
41688
|
predicateMatch,
|
|
40846
41689
|
predicateNode: unwrappedExpression
|
|
40847
41690
|
};
|
|
40848
|
-
if (isNodeOfType(unwrappedExpression, "
|
|
40849
|
-
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
40853
|
-
|
|
40854
|
-
|
|
40855
|
-
|
|
41691
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
41692
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
41693
|
+
const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
|
|
41694
|
+
if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
|
|
41695
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41696
|
+
const match = matchHydrationConditionInternal(parameterValue, context, state);
|
|
41697
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41698
|
+
return match;
|
|
41699
|
+
}
|
|
41700
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
|
|
41701
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41702
|
+
const match = matchHydrationConditionInternal(symbol.initializer, context, state);
|
|
41703
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41704
|
+
return match;
|
|
40856
41705
|
}
|
|
41706
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41707
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41708
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41709
|
+
allowGlobalReactNamespace: true,
|
|
41710
|
+
resolveNamedAliases: true
|
|
41711
|
+
})) {
|
|
41712
|
+
const callbackArgument = callArguments[0];
|
|
41713
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41714
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41715
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
|
|
41716
|
+
}
|
|
41717
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41718
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
|
|
41719
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41720
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41721
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41722
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41723
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41724
|
+
const argument = callArguments[parameterIndex];
|
|
41725
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41726
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41727
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41728
|
+
}
|
|
41729
|
+
return matchHydrationFunctionResult(helperFunction, context, {
|
|
41730
|
+
...state,
|
|
41731
|
+
parameterValuesBySymbolId
|
|
41732
|
+
});
|
|
41733
|
+
}
|
|
41734
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
|
|
41735
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
41736
|
+
const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
|
|
41737
|
+
const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
|
|
40857
41738
|
const nestedMatch = leftMatch ?? rightMatch;
|
|
40858
41739
|
if (!nestedMatch) return null;
|
|
40859
|
-
const
|
|
40860
|
-
|
|
40861
|
-
return nestedMatch;
|
|
41740
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
|
|
41741
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
|
|
41742
|
+
return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
|
|
40862
41743
|
};
|
|
41744
|
+
const matchHydrationReturningStatement = (statement, context, state) => {
|
|
41745
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
|
|
41746
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
41747
|
+
const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
|
|
41748
|
+
const consequentValues = getReturnedValues(statement.consequent);
|
|
41749
|
+
const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
|
|
41750
|
+
if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
|
|
41751
|
+
return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
|
|
41752
|
+
}
|
|
41753
|
+
if (!isNodeOfType(statement, "BlockStatement")) return null;
|
|
41754
|
+
for (const childStatement of statement.body) {
|
|
41755
|
+
const match = matchHydrationReturningStatement(childStatement, context, state);
|
|
41756
|
+
if (match) return match;
|
|
41757
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41758
|
+
}
|
|
41759
|
+
return null;
|
|
41760
|
+
};
|
|
41761
|
+
const matchHydrationFunctionResult = (functionNode, context, state) => {
|
|
41762
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41763
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41764
|
+
const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
|
|
41765
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41766
|
+
return match;
|
|
41767
|
+
};
|
|
41768
|
+
const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
|
|
41769
|
+
parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
|
|
41770
|
+
visitedFunctionNodes: /* @__PURE__ */ new Set(),
|
|
41771
|
+
visitedSymbolIds: /* @__PURE__ */ new Set()
|
|
41772
|
+
});
|
|
40863
41773
|
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
40864
41774
|
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
40865
41775
|
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
@@ -41002,17 +41912,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
41002
41912
|
const { predicateMatch, predicateNode } = conditionMatch;
|
|
41003
41913
|
if (reportedNodes.has(predicateNode)) return;
|
|
41004
41914
|
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
41005
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(
|
|
41915
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
41006
41916
|
if (!componentOrHookNode) return;
|
|
41007
41917
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
41008
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41918
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41009
41919
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
41010
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41920
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
41011
41921
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
41012
41922
|
}
|
|
41013
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
41014
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
41015
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
41923
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
|
|
41924
|
+
if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41925
|
+
const openingElement = findEnclosingJsxOpeningElement(conditionNode);
|
|
41016
41926
|
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
41017
41927
|
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
41018
41928
|
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
@@ -41394,7 +42304,7 @@ const noInitializeState = defineRule({
|
|
|
41394
42304
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41395
42305
|
const analysis = getProgramAnalysis(node);
|
|
41396
42306
|
if (!analysis) return;
|
|
41397
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
42307
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
41398
42308
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
41399
42309
|
const stateName = getStateName(fact.stateDeclarator);
|
|
41400
42310
|
context.report({
|
|
@@ -41752,7 +42662,8 @@ const noJsxElementType = defineRule({
|
|
|
41752
42662
|
create: (context) => {
|
|
41753
42663
|
let isJsxImported = false;
|
|
41754
42664
|
const flaggedAnnotations = [];
|
|
41755
|
-
const
|
|
42665
|
+
const collectComponentReturnType = (functionNode, returnType) => {
|
|
42666
|
+
if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
|
|
41756
42667
|
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
41757
42668
|
if (!typeAnnotation) return;
|
|
41758
42669
|
if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
|
|
@@ -41762,19 +42673,16 @@ const noJsxElementType = defineRule({
|
|
|
41762
42673
|
if (isJsxImportBinding(node)) isJsxImported = true;
|
|
41763
42674
|
},
|
|
41764
42675
|
FunctionDeclaration(node) {
|
|
41765
|
-
|
|
42676
|
+
collectComponentReturnType(node, node.returnType);
|
|
41766
42677
|
},
|
|
41767
42678
|
ArrowFunctionExpression(node) {
|
|
41768
|
-
|
|
42679
|
+
collectComponentReturnType(node, node.returnType);
|
|
41769
42680
|
},
|
|
41770
42681
|
FunctionExpression(node) {
|
|
41771
|
-
|
|
42682
|
+
collectComponentReturnType(node, node.returnType);
|
|
41772
42683
|
},
|
|
41773
42684
|
TSDeclareFunction(node) {
|
|
41774
|
-
|
|
41775
|
-
},
|
|
41776
|
-
TSMethodSignature(node) {
|
|
41777
|
-
checkReturnType(node.returnType);
|
|
42685
|
+
collectComponentReturnType(node, node.returnType);
|
|
41778
42686
|
},
|
|
41779
42687
|
"Program:exit"() {
|
|
41780
42688
|
if (isJsxImported) return;
|
|
@@ -45308,6 +46216,7 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
|
45308
46216
|
"useMatchMedia",
|
|
45309
46217
|
"useMediaJobProgress",
|
|
45310
46218
|
"useMediaQuery",
|
|
46219
|
+
"useMediaQueryState",
|
|
45311
46220
|
"useResizeObserver",
|
|
45312
46221
|
"useVisibility",
|
|
45313
46222
|
"useWindowSize"
|
|
@@ -45344,9 +46253,30 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
45344
46253
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
45345
46254
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
45346
46255
|
};
|
|
45347
|
-
const
|
|
46256
|
+
const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
46257
|
+
let hookFunction = resolveToFunction(ref);
|
|
46258
|
+
if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
|
|
46259
|
+
const definitionNode = definition.node;
|
|
46260
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
|
|
46261
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
46262
|
+
if (!isNodeOfType(initializer, "CallExpression")) continue;
|
|
46263
|
+
const callee = stripParenExpression(initializer.callee);
|
|
46264
|
+
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
46265
|
+
const calleeReference = getRef(analysis, callee);
|
|
46266
|
+
if (!calleeReference) continue;
|
|
46267
|
+
hookFunction = resolveToFunction(calleeReference);
|
|
46268
|
+
if (hookFunction) break;
|
|
46269
|
+
}
|
|
46270
|
+
if (!hookFunction) return null;
|
|
46271
|
+
const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
|
|
46272
|
+
if (returnedReferences.length === 0) return null;
|
|
46273
|
+
return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
|
|
46274
|
+
};
|
|
46275
|
+
const isExternalSubscriptionHookRef = (analysis, ref) => {
|
|
45348
46276
|
const identifier = ref.identifier;
|
|
45349
46277
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
46278
|
+
const localHookProof = getLocalHookExternalStateProof(analysis, ref);
|
|
46279
|
+
if (localHookProof !== null) return localHookProof;
|
|
45350
46280
|
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45351
46281
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
45352
46282
|
const node = def.node;
|
|
@@ -45430,11 +46360,11 @@ const noPassDataToParent = defineRule({
|
|
|
45430
46360
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
45431
46361
|
}
|
|
45432
46362
|
return getDownstreamRefs(analysis, argument);
|
|
45433
|
-
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
46363
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
45434
46364
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45435
46365
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
45436
46366
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
45437
|
-
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
46367
|
+
if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
|
|
45438
46368
|
if (isProp(analysis, argRef)) return false;
|
|
45439
46369
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
45440
46370
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -46998,6 +47928,69 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
46998
47928
|
}
|
|
46999
47929
|
});
|
|
47000
47930
|
//#endregion
|
|
47931
|
+
//#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
|
|
47932
|
+
const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47933
|
+
const expression = stripParenExpression(rawExpression);
|
|
47934
|
+
if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
|
|
47935
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
47936
|
+
if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
|
|
47937
|
+
return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
|
|
47938
|
+
}
|
|
47939
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
47940
|
+
if (isNodeOfType(expression.left, "Literal")) {
|
|
47941
|
+
const isLeftTruthy = Boolean(expression.left.value);
|
|
47942
|
+
if (expression.operator === "&&" && !isLeftTruthy) return [];
|
|
47943
|
+
if (expression.operator === "||" && isLeftTruthy) return [];
|
|
47944
|
+
if (expression.operator === "??" && expression.left.value !== null) return [];
|
|
47945
|
+
}
|
|
47946
|
+
if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
|
|
47947
|
+
return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
|
|
47948
|
+
}
|
|
47949
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
47950
|
+
const finalExpression = expression.expressions.at(-1);
|
|
47951
|
+
return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
|
|
47952
|
+
}
|
|
47953
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
47954
|
+
if (!isReactApiCall(expression, "useCallback", scopes)) return [];
|
|
47955
|
+
const callback = expression.arguments[0];
|
|
47956
|
+
return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
|
|
47957
|
+
}
|
|
47958
|
+
if (!isNodeOfType(expression, "Identifier")) return [];
|
|
47959
|
+
const symbol = scopes.symbolFor(expression);
|
|
47960
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
|
|
47961
|
+
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47962
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
47963
|
+
if (!initializer) return [];
|
|
47964
|
+
return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47965
|
+
};
|
|
47966
|
+
const functionReturnsCleanupFunction = (functionExpression, scopes) => {
|
|
47967
|
+
if (!isFunctionLike$1(functionExpression)) return false;
|
|
47968
|
+
if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
|
|
47969
|
+
return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
|
|
47970
|
+
};
|
|
47971
|
+
const callbackReturnsCleanupFunction = (callback, scopes) => {
|
|
47972
|
+
return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
|
|
47973
|
+
};
|
|
47974
|
+
const noRefCallbackCleanupBeforeReact19 = defineRule({
|
|
47975
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
47976
|
+
title: "Ref cleanup requires React 19",
|
|
47977
|
+
requires: ["react:18"],
|
|
47978
|
+
disabledWhen: ["react:19"],
|
|
47979
|
+
severity: "warn",
|
|
47980
|
+
recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
|
|
47981
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
47982
|
+
if (getJsxAttributeName(node.name) !== "ref") return;
|
|
47983
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
47984
|
+
const callback = node.value.expression;
|
|
47985
|
+
if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
|
|
47986
|
+
if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
|
|
47987
|
+
context.report({
|
|
47988
|
+
node,
|
|
47989
|
+
message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
|
|
47990
|
+
});
|
|
47991
|
+
} })
|
|
47992
|
+
});
|
|
47993
|
+
//#endregion
|
|
47001
47994
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
47002
47995
|
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
47003
47996
|
"DoWhileStatement",
|
|
@@ -56959,8 +57952,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
|
|
|
56959
57952
|
destructureIndex: 1
|
|
56960
57953
|
});
|
|
56961
57954
|
//#endregion
|
|
57955
|
+
//#region src/plugin/utils/unwrap-return-expression.ts
|
|
57956
|
+
const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
|
|
57957
|
+
//#endregion
|
|
56962
57958
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
56963
57959
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
57960
|
+
const USE_CALLBACK_ONLY = new Set(["useCallback"]);
|
|
57961
|
+
const USE_STATE_ONLY = new Set(["useState"]);
|
|
57962
|
+
const REACT_API_CALL_OPTIONS = {
|
|
57963
|
+
allowGlobalReactNamespace: true,
|
|
57964
|
+
allowUnboundBareCalls: true,
|
|
57965
|
+
resolveNamedAliases: true
|
|
57966
|
+
};
|
|
57967
|
+
const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
|
|
57968
|
+
let readsDerivedSymbol = false;
|
|
57969
|
+
walkAst(expression, (node) => {
|
|
57970
|
+
if (readsDerivedSymbol) return false;
|
|
57971
|
+
if (node !== expression && isFunctionLike$1(node)) return false;
|
|
57972
|
+
if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
|
|
57973
|
+
});
|
|
57974
|
+
return readsDerivedSymbol;
|
|
57975
|
+
};
|
|
57976
|
+
const getStaticObjectPropertyName = (property) => {
|
|
57977
|
+
if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
|
|
57978
|
+
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
57979
|
+
if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
|
|
57980
|
+
return null;
|
|
57981
|
+
};
|
|
57982
|
+
const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
|
|
57983
|
+
const isTransparentAssignmentTarget = (identifier) => {
|
|
57984
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
57985
|
+
const parent = expressionRoot.parent;
|
|
57986
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
|
|
57987
|
+
};
|
|
56964
57988
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
56965
57989
|
let readsCurrent = false;
|
|
56966
57990
|
walkAst(argument, (child) => {
|
|
@@ -57012,6 +58036,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
|
|
|
57012
58036
|
});
|
|
57013
58037
|
return referenceCount > 0 && !nonAriaReferenceFound;
|
|
57014
58038
|
};
|
|
58039
|
+
const isGlobalWindowMember = (context, node, propertyName) => {
|
|
58040
|
+
const member = stripParenExpression(node);
|
|
58041
|
+
if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
|
|
58042
|
+
const receiver = stripParenExpression(member.object);
|
|
58043
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
|
|
58044
|
+
};
|
|
58045
|
+
const getDirectWindowWidthSetter = (context, statement) => {
|
|
58046
|
+
const call = unwrapDiscardedExpression(statement);
|
|
58047
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
|
|
58048
|
+
if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
|
|
58049
|
+
const argument = call.arguments[0];
|
|
58050
|
+
return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
|
|
58051
|
+
};
|
|
58052
|
+
const getResizeListenerHandler = (context, statement, methodName) => {
|
|
58053
|
+
const call = unwrapDiscardedExpression(statement);
|
|
58054
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
|
|
58055
|
+
if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
|
|
58056
|
+
const eventName = call.arguments[0];
|
|
58057
|
+
const handler = call.arguments[1];
|
|
58058
|
+
if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
|
|
58059
|
+
return isNodeOfType(handler, "Identifier") ? handler : null;
|
|
58060
|
+
};
|
|
58061
|
+
const getCleanupResizeHandler = (context, statement) => {
|
|
58062
|
+
if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
|
|
58063
|
+
const cleanupStatements = getCallbackStatements(statement.argument);
|
|
58064
|
+
if (cleanupStatements.length !== 1) return null;
|
|
58065
|
+
return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
|
|
58066
|
+
};
|
|
58067
|
+
const findExactViewportState = (context, componentFunction, setterCall) => {
|
|
58068
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
58069
|
+
const componentBody = componentFunction.body;
|
|
58070
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
|
|
58071
|
+
const setterSymbol = context.scopes.symbolFor(setterCall.callee);
|
|
58072
|
+
if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
58073
|
+
const declarator = setterSymbol.declarationNode;
|
|
58074
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
58075
|
+
const stateIdentifier = declarator.id.elements?.[0];
|
|
58076
|
+
const setterIdentifier = declarator.id.elements?.[1];
|
|
58077
|
+
if (!isNodeOfType(stateIdentifier, "Identifier") || !isNodeOfType(setterIdentifier, "Identifier") || setterIdentifier !== setterSymbol.bindingIdentifier || !isNodeOfType(declarator.init, "CallExpression") || !isReactApiCall(declarator.init, USE_STATE_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return null;
|
|
58078
|
+
const initializer = declarator.init.arguments?.[0];
|
|
58079
|
+
if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
|
|
58080
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
58081
|
+
if (!stateSymbol) return null;
|
|
58082
|
+
const stateDerivedSymbolIds = new Set([stateSymbol.id]);
|
|
58083
|
+
let didAddDerivedSymbol = true;
|
|
58084
|
+
while (didAddDerivedSymbol) {
|
|
58085
|
+
didAddDerivedSymbol = false;
|
|
58086
|
+
for (const statement of componentBody.body ?? []) {
|
|
58087
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
58088
|
+
for (const candidateDeclarator of statement.declarations ?? []) {
|
|
58089
|
+
if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
|
|
58090
|
+
const candidateInitializer = stripParenExpression(candidateDeclarator.init);
|
|
58091
|
+
if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
|
|
58092
|
+
if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
|
|
58093
|
+
const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
|
|
58094
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
|
|
58095
|
+
stateDerivedSymbolIds.add(candidateSymbol.id);
|
|
58096
|
+
didAddDerivedSymbol = true;
|
|
58097
|
+
}
|
|
58098
|
+
}
|
|
58099
|
+
}
|
|
58100
|
+
}
|
|
58101
|
+
const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
|
|
58102
|
+
const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
58103
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
58104
|
+
if (!symbol) return false;
|
|
58105
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
58106
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
58107
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
58108
|
+
let hasUnknownReference = false;
|
|
58109
|
+
walkAst(componentBody, (node) => {
|
|
58110
|
+
if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
|
|
58111
|
+
const referenceRoot = findTransparentExpressionRoot(node);
|
|
58112
|
+
const parent = referenceRoot.parent;
|
|
58113
|
+
if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
|
|
58114
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
|
|
58115
|
+
hasUnknownReference = true;
|
|
58116
|
+
return false;
|
|
58117
|
+
});
|
|
58118
|
+
return !hasUnknownReference;
|
|
58119
|
+
};
|
|
58120
|
+
const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
58121
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
58122
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
|
|
58123
|
+
const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
|
|
58124
|
+
if (cachedVisibility) return cachedVisibility;
|
|
58125
|
+
if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
|
|
58126
|
+
if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
|
|
58127
|
+
const initializer = stripParenExpression(symbol.declarationNode.init);
|
|
58128
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
58129
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
58130
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
58131
|
+
const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
|
|
58132
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
58133
|
+
return visibility;
|
|
58134
|
+
}
|
|
58135
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
|
|
58136
|
+
let visibility = "non-visible";
|
|
58137
|
+
for (const property of initializer.properties ?? []) {
|
|
58138
|
+
const propertyName = getStaticObjectPropertyName(property);
|
|
58139
|
+
if (!isNodeOfType(property, "Property") || !propertyName) {
|
|
58140
|
+
visibility = "unknown";
|
|
58141
|
+
break;
|
|
58142
|
+
}
|
|
58143
|
+
if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
|
|
58144
|
+
}
|
|
58145
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
58146
|
+
return visibility;
|
|
58147
|
+
};
|
|
58148
|
+
let hasNonAriaReference = false;
|
|
58149
|
+
walkAst(componentBody, (node) => {
|
|
58150
|
+
if (hasNonAriaReference) return false;
|
|
58151
|
+
if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
|
|
58152
|
+
if (findEnclosingFunction$1(node) !== componentFunction) return;
|
|
58153
|
+
const parent = node.parent;
|
|
58154
|
+
if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
|
|
58155
|
+
let cursor = parent;
|
|
58156
|
+
while (cursor && cursor !== componentBody) {
|
|
58157
|
+
if (isFunctionLike$1(cursor)) return;
|
|
58158
|
+
if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
|
|
58159
|
+
if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
|
|
58160
|
+
return;
|
|
58161
|
+
}
|
|
58162
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
58163
|
+
if (isEventHandlerAttribute(cursor)) return;
|
|
58164
|
+
if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
|
|
58165
|
+
return;
|
|
58166
|
+
}
|
|
58167
|
+
if (isNodeOfType(cursor, "ReturnStatement")) {
|
|
58168
|
+
hasNonAriaReference = true;
|
|
58169
|
+
return;
|
|
58170
|
+
}
|
|
58171
|
+
cursor = cursor.parent;
|
|
58172
|
+
}
|
|
58173
|
+
});
|
|
58174
|
+
return hasNonAriaReference ? stateIdentifier.name : null;
|
|
58175
|
+
};
|
|
58176
|
+
const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
|
|
58177
|
+
if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
|
|
58178
|
+
if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
58179
|
+
const statements = getCallbackStatements(callback);
|
|
58180
|
+
if (statements.length !== 4) return false;
|
|
58181
|
+
const handlerDeclaration = statements[0];
|
|
58182
|
+
if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
|
|
58183
|
+
const handlerDeclarator = handlerDeclaration.declarations[0];
|
|
58184
|
+
if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
|
|
58185
|
+
const handlerStatements = getCallbackStatements(handlerDeclarator.init);
|
|
58186
|
+
if (handlerStatements.length !== 1) return false;
|
|
58187
|
+
const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
|
|
58188
|
+
const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
|
|
58189
|
+
const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
|
|
58190
|
+
const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
|
|
58191
|
+
if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
|
|
58192
|
+
const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
|
|
58193
|
+
if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
|
|
58194
|
+
if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
|
|
58195
|
+
const componentFunction = findEnclosingFunction$1(effectCall);
|
|
58196
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
|
|
58197
|
+
return findExactViewportState(context, componentFunction, immediateSetter) !== null;
|
|
58198
|
+
};
|
|
57015
58199
|
const renderingHydrationNoFlicker = defineRule({
|
|
57016
58200
|
id: "rendering-hydration-no-flicker",
|
|
57017
58201
|
title: "useEffect setState flashes on mount",
|
|
@@ -57024,7 +58208,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
57024
58208
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
57025
58209
|
const callback = getEffectCallback(node);
|
|
57026
58210
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
57027
|
-
|
|
58211
|
+
if (isExactViewportSubscriptionEffect(context, node, callback)) {
|
|
58212
|
+
context.report({
|
|
58213
|
+
node,
|
|
58214
|
+
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
58215
|
+
});
|
|
58216
|
+
return;
|
|
58217
|
+
}
|
|
58218
|
+
const bodyStatements = getCallbackStatements(callback);
|
|
57028
58219
|
if (bodyStatements.length !== 1) return;
|
|
57029
58220
|
const soleStatement = bodyStatements[0];
|
|
57030
58221
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
@@ -71567,6 +72758,17 @@ const reactDoctorRules = [
|
|
|
71567
72758
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
71568
72759
|
}
|
|
71569
72760
|
},
|
|
72761
|
+
{
|
|
72762
|
+
key: "react-doctor/no-ref-callback-cleanup-before-react-19",
|
|
72763
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
72764
|
+
source: "react-doctor",
|
|
72765
|
+
originallyExternal: false,
|
|
72766
|
+
rule: {
|
|
72767
|
+
...noRefCallbackCleanupBeforeReact19,
|
|
72768
|
+
framework: "global",
|
|
72769
|
+
category: "Bugs"
|
|
72770
|
+
}
|
|
72771
|
+
},
|
|
71570
72772
|
{
|
|
71571
72773
|
key: "react-doctor/no-ref-current-in-render",
|
|
71572
72774
|
id: "no-ref-current-in-render",
|