oxlint-plugin-react-doctor 0.7.9-dev.2ba83c3 → 0.7.9-dev.44d3ad8

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +46 -0
  2. package/dist/index.js +1788 -418
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -836,22 +836,131 @@ const getImportedName = (importSpecifier) => {
836
836
  if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
837
837
  };
838
838
  //#endregion
839
- //#region src/plugin/utils/get-callee-name.ts
840
- const getCalleeName$2 = (node) => {
841
- if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
842
- if (isNodeOfType(node.callee, "Identifier")) return node.callee.name;
843
- if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier")) return node.callee.property.name;
839
+ //#region src/plugin/utils/get-static-property-key-name.ts
840
+ const getStaticPropertyKeyName = (node, options = {}) => {
841
+ if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
842
+ const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
843
+ if (node.computed) {
844
+ if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
845
+ if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
846
+ return null;
847
+ }
848
+ if (isNodeOfType(key, "Identifier")) return key.name;
849
+ if (isNodeOfType(key, "Literal")) {
850
+ if (typeof key.value === "string") return key.value;
851
+ if (options.stringifyNonStringLiterals) return String(key.value);
852
+ }
844
853
  return null;
845
854
  };
846
855
  //#endregion
847
- //#region src/plugin/utils/is-hook-call.ts
848
- const isHookCall$2 = (node, hookName) => {
856
+ //#region src/plugin/utils/get-static-property-name.ts
857
+ const getStaticPropertyName = (memberExpression) => {
858
+ const property = memberExpression.property;
859
+ if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
860
+ if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
861
+ if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
862
+ return null;
863
+ };
864
+ //#endregion
865
+ //#region src/plugin/utils/strip-paren-expression.ts
866
+ const TRANSPARENT_EXPRESSION_WRAPPER_TYPES = new Set([
867
+ "ParenthesizedExpression",
868
+ "TSAsExpression",
869
+ "TSSatisfiesExpression",
870
+ "TSTypeAssertion",
871
+ "TSNonNullExpression",
872
+ "TSInstantiationExpression",
873
+ "ChainExpression"
874
+ ]);
875
+ const stripParenExpression = (node) => {
876
+ let current = node;
877
+ while (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) current = current.expression;
878
+ return current;
879
+ };
880
+ //#endregion
881
+ //#region src/plugin/utils/resolve-const-identifier-alias.ts
882
+ const resolveConstIdentifierAlias = (identifier, scopes, allowPatternBinding = false) => {
883
+ if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
884
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
885
+ let symbol = scopes.symbolFor(identifier);
886
+ while (symbol?.kind === "const") {
887
+ if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return null;
888
+ if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
889
+ visitedSymbolIds.add(symbol.id);
890
+ const initializer = stripParenExpression(symbol.initializer);
891
+ if (!isNodeOfType(initializer, "Identifier")) return symbol;
892
+ symbol = scopes.symbolFor(initializer);
893
+ }
894
+ return symbol;
895
+ };
896
+ //#endregion
897
+ //#region src/plugin/utils/is-react-api-call.ts
898
+ const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
899
+ const isImportedFromReact = (symbol) => {
900
+ if (symbol.kind !== "import") return false;
901
+ const importDeclaration = symbol.declarationNode.parent;
902
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
903
+ };
904
+ const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
905
+ if (!isNodeOfType(identifier, "Identifier")) return false;
906
+ const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
907
+ if (!symbol || !isImportedFromReact(symbol)) return false;
908
+ const importedName = getImportedName(symbol.declarationNode);
909
+ return Boolean(importedName && includesApiName(apiNames, importedName));
910
+ };
911
+ const isReactNamespaceImport = (identifier, scopes) => {
912
+ const symbol = resolveConstIdentifierAlias(identifier, scopes);
913
+ if (!symbol || !isImportedFromReact(symbol)) return false;
914
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
915
+ };
916
+ const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
917
+ if (!isNodeOfType(receiver, "Identifier")) return false;
918
+ if (isReactNamespaceImport(receiver, scopes)) return true;
919
+ return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
920
+ };
921
+ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
922
+ const symbol = scopes.symbolFor(identifier);
923
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
924
+ const pattern = symbol.declarationNode.id;
925
+ if (!isNodeOfType(pattern, "ObjectPattern")) return false;
926
+ for (const property of pattern.properties) {
927
+ if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
928
+ const propertyName = getStaticPropertyKeyName(property);
929
+ return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
930
+ }
931
+ return false;
932
+ };
933
+ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
849
934
  if (!isNodeOfType(node, "CallExpression")) return false;
850
- const calleeName = getCalleeName$2(node);
851
- if (!calleeName) return false;
852
- return typeof hookName === "string" ? calleeName === hookName : hookName.has(calleeName);
935
+ return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
936
+ };
937
+ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
938
+ const callee = stripParenExpression(rawCallee);
939
+ if (options.resolveConditionalAliases && isNodeOfType(callee, "ConditionalExpression")) return isReactApiCallee(callee.consequent, apiNames, scopes, options, new Set(visitedSymbolIds)) && isReactApiCallee(callee.alternate, apiNames, scopes, options, new Set(visitedSymbolIds));
940
+ if (isNodeOfType(callee, "Identifier")) {
941
+ if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
942
+ if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
943
+ if (options.resolveConditionalAliases) {
944
+ const symbol = scopes.symbolFor(callee);
945
+ if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
946
+ visitedSymbolIds.add(symbol.id);
947
+ return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
948
+ }
949
+ }
950
+ return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
951
+ }
952
+ if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
953
+ return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
853
954
  };
854
955
  //#endregion
956
+ //#region src/plugin/utils/is-react-hook-call.ts
957
+ const isReactHookCall = (node, hookNames, scopes) => isReactApiCall(node, hookNames, scopes, {
958
+ allowGlobalReactNamespace: true,
959
+ allowUnboundBareCalls: true,
960
+ resolveConditionalAliases: true,
961
+ resolveNamedAliases: true
962
+ });
963
+ //#endregion
855
964
  //#region src/plugin/utils/is-ast-node.ts
856
965
  const isAstNode = (value) => value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
857
966
  //#endregion
@@ -921,12 +1030,12 @@ const collectChildComponentNames = (element, into) => {
921
1030
  into.add(name);
922
1031
  });
923
1032
  };
924
- const countEffectHookCalls = (body) => {
1033
+ const countEffectHookCalls = (body, scopes) => {
925
1034
  if (!body) return 0;
926
1035
  let count = 0;
927
1036
  walkAst(body, (child) => {
928
1037
  if (!isNodeOfType(child, "CallExpression")) return;
929
- if (isHookCall$2(child, EFFECT_HOOK_NAMES$1)) count++;
1038
+ if (isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) count++;
930
1039
  });
931
1040
  return count;
932
1041
  };
@@ -952,11 +1061,11 @@ const getComponentEffectIndex = (programRoot) => {
952
1061
  componentEffectIndexCache.set(programRoot, index);
953
1062
  return index;
954
1063
  };
955
- const getSameFileComponentEffectCount = (programRoot, componentName) => {
1064
+ const getSameFileComponentEffectCount = (programRoot, componentName, scopes) => {
956
1065
  const index = getComponentEffectIndex(programRoot);
957
1066
  const cachedCount = index.effectCountByName.get(componentName);
958
1067
  if (cachedCount !== void 0) return cachedCount;
959
- const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
1068
+ const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null, scopes);
960
1069
  index.effectCountByName.set(componentName, count);
961
1070
  return count;
962
1071
  };
@@ -1016,7 +1125,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
1016
1125
  let totalEffects = 0;
1017
1126
  const effectfulChildren = [];
1018
1127
  for (const componentName of childComponentNames) {
1019
- const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
1128
+ const effectCount = getSameFileComponentEffectCount(programRoot, componentName, context.scopes);
1020
1129
  if (effectCount === 0) continue;
1021
1130
  totalEffects += effectCount;
1022
1131
  effectfulChildren.push(`<${componentName}>`);
@@ -1207,6 +1316,14 @@ const findVariableInitializer = (referenceNode, bindingName) => {
1207
1316
  return best;
1208
1317
  };
1209
1318
  //#endregion
1319
+ //#region src/plugin/utils/get-callee-name.ts
1320
+ const getCalleeName$1 = (node) => {
1321
+ if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
1322
+ if (isNodeOfType(node.callee, "Identifier")) return node.callee.name;
1323
+ if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier")) return node.callee.property.name;
1324
+ return null;
1325
+ };
1326
+ //#endregion
1210
1327
  //#region src/plugin/utils/is-function-like.ts
1211
1328
  /**
1212
1329
  * Type-guard for the three "function-like" ESTree node shapes:
@@ -1220,37 +1337,6 @@ const findVariableInitializer = (referenceNode, bindingName) => {
1220
1337
  */
1221
1338
  const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
1222
1339
  //#endregion
1223
- //#region src/plugin/utils/strip-paren-expression.ts
1224
- const TRANSPARENT_EXPRESSION_WRAPPER_TYPES = new Set([
1225
- "ParenthesizedExpression",
1226
- "TSAsExpression",
1227
- "TSSatisfiesExpression",
1228
- "TSTypeAssertion",
1229
- "TSNonNullExpression",
1230
- "TSInstantiationExpression",
1231
- "ChainExpression"
1232
- ]);
1233
- const stripParenExpression = (node) => {
1234
- let current = node;
1235
- while (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) current = current.expression;
1236
- return current;
1237
- };
1238
- //#endregion
1239
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
1240
- const resolveConstIdentifierAlias = (identifier, scopes) => {
1241
- if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
1242
- const visitedSymbolIds = /* @__PURE__ */ new Set();
1243
- let symbol = scopes.symbolFor(identifier);
1244
- while (symbol?.kind === "const") {
1245
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
1246
- visitedSymbolIds.add(symbol.id);
1247
- const initializer = stripParenExpression(symbol.initializer);
1248
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
1249
- symbol = scopes.symbolFor(initializer);
1250
- }
1251
- return symbol;
1252
- };
1253
- //#endregion
1254
1340
  //#region src/plugin/utils/resolve-exact-local-function.ts
1255
1341
  const resolveExactLocalFunction = (expression, scopes) => {
1256
1342
  const unwrappedExpression = stripParenExpression(expression);
@@ -1296,9 +1382,17 @@ const getRootIdentifier$1 = (node, options) => {
1296
1382
  //#region src/plugin/utils/get-root-identifier-name.ts
1297
1383
  const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
1298
1384
  //#endregion
1385
+ //#region src/plugin/utils/is-hook-call.ts
1386
+ const isHookCall$2 = (node, hookName) => {
1387
+ if (!isNodeOfType(node, "CallExpression")) return false;
1388
+ const calleeName = getCalleeName$1(node);
1389
+ if (!calleeName) return false;
1390
+ return typeof hookName === "string" ? calleeName === hookName : hookName.has(calleeName);
1391
+ };
1392
+ //#endregion
1299
1393
  //#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
1300
- const STABLE_HANDLER_HOOK_NAMES = new Set([
1301
- "useCallback",
1394
+ const REACT_STABLE_HANDLER_HOOK_NAMES = new Set(["useCallback", "useEffectEvent"]);
1395
+ const CUSTOM_STABLE_HANDLER_HOOK_NAMES = new Set([
1302
1396
  "useEffectEvent",
1303
1397
  "useEvent",
1304
1398
  "useEventCallback",
@@ -1307,21 +1401,21 @@ const STABLE_HANDLER_HOOK_NAMES = new Set([
1307
1401
  ]);
1308
1402
  const THROTTLED_HANDLER_HOOK_PATTERN = /^use\w*(?:Throttle|Debounce)/i;
1309
1403
  const isThrottledHandlerHookCall = (callNode) => {
1310
- const calleeName = getCalleeName$2(callNode);
1404
+ const calleeName = getCalleeName$1(callNode);
1311
1405
  return calleeName !== null && THROTTLED_HANDLER_HOOK_PATTERN.test(calleeName);
1312
1406
  };
1313
- const isEmptyDepsUseMemoCall = (callNode) => {
1314
- if (!isHookCall$2(callNode, "useMemo")) return false;
1407
+ const isEmptyDepsUseMemoCall = (callNode, scopes) => {
1408
+ if (!isReactHookCall(callNode, "useMemo", scopes)) return false;
1315
1409
  const memoDepsNode = callNode.arguments?.[1];
1316
1410
  return isNodeOfType(memoDepsNode, "ArrayExpression") && (memoDepsNode.elements?.length ?? 0) === 0;
1317
1411
  };
1318
- const isStableHandlerInitializer = (initializer) => {
1319
- if (isNodeOfType(initializer, "CallExpression")) return isHookCall$2(initializer, STABLE_HANDLER_HOOK_NAMES) || isEmptyDepsUseMemoCall(initializer) || isThrottledHandlerHookCall(initializer);
1412
+ const isStableHandlerInitializer = (initializer, scopes) => {
1413
+ if (isNodeOfType(initializer, "CallExpression")) return isReactHookCall(initializer, REACT_STABLE_HANDLER_HOOK_NAMES, scopes) || isHookCall$2(initializer, CUSTOM_STABLE_HANDLER_HOOK_NAMES) || isEmptyDepsUseMemoCall(initializer, scopes) || isThrottledHandlerHookCall(initializer);
1320
1414
  return isNodeOfType(initializer, "MemberExpression") && isNodeOfType(initializer.property, "Identifier") && initializer.property.name === "current";
1321
1415
  };
1322
- const isStableRefReceiverDep = (referenceNode, receiverDepName) => {
1416
+ const isStableRefReceiverDep = (referenceNode, receiverDepName, scopes) => {
1323
1417
  const receiverBinding = findVariableInitializer(referenceNode, receiverDepName);
1324
- return Boolean(receiverBinding?.initializer && isHookCall$2(receiverBinding.initializer, "useRef"));
1418
+ return Boolean(receiverBinding?.initializer && isReactHookCall(receiverBinding.initializer, "useRef", scopes));
1325
1419
  };
1326
1420
  const advancedEventHandlerRefs = defineRule({
1327
1421
  id: "advanced-event-handler-refs",
@@ -1331,7 +1425,7 @@ const advancedEventHandlerRefs = defineRule({
1331
1425
  category: "Performance",
1332
1426
  recommendation: "Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",
1333
1427
  create: (context) => ({ CallExpression(node) {
1334
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
1428
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
1335
1429
  if ((node.arguments?.length ?? 0) < 2) return;
1336
1430
  const callback = getEffectCallback(node);
1337
1431
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -1355,8 +1449,8 @@ const advancedEventHandlerRefs = defineRule({
1355
1449
  });
1356
1450
  if (!registeredHandlerName) return;
1357
1451
  const handlerBinding = findVariableInitializer(node, registeredHandlerName);
1358
- if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer)) return;
1359
- if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName))) return;
1452
+ if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer, context.scopes)) return;
1453
+ if ([...depIdentifierNames].some((depName) => depName !== registeredHandlerName && subscriptionReceiverNames.has(depName) && !isStableRefReceiverDep(node, depName, context.scopes))) return;
1360
1454
  context.report({
1361
1455
  node,
1362
1456
  message: `useEffect re-adds the "${registeredHandlerName}" listener every time the handler changes.`
@@ -1843,15 +1937,6 @@ const flattenJsxName$1 = (node) => {
1843
1937
  return null;
1844
1938
  };
1845
1939
  //#endregion
1846
- //#region src/plugin/utils/get-static-property-name.ts
1847
- const getStaticPropertyName = (memberExpression) => {
1848
- const property = memberExpression.property;
1849
- if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
1850
- if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
1851
- if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
1852
- return null;
1853
- };
1854
- //#endregion
1855
1940
  //#region src/plugin/utils/is-generated-image-renderer-call.ts
1856
1941
  const GENERATED_IMAGE_RENDERER_MODULES = [
1857
1942
  "next/og",
@@ -4859,23 +4944,6 @@ const containsDirectAwait = (node) => {
4859
4944
  return foundAwait;
4860
4945
  };
4861
4946
  //#endregion
4862
- //#region src/plugin/utils/get-static-property-key-name.ts
4863
- const getStaticPropertyKeyName = (node, options = {}) => {
4864
- if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
4865
- const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
4866
- if (node.computed) {
4867
- if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
4868
- if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
4869
- return null;
4870
- }
4871
- if (isNodeOfType(key, "Identifier")) return key.name;
4872
- if (isNodeOfType(key, "Literal")) {
4873
- if (typeof key.value === "string") return key.value;
4874
- if (options.stringifyNonStringLiterals) return String(key.value);
4875
- }
4876
- return null;
4877
- };
4878
- //#endregion
4879
4947
  //#region src/plugin/utils/get-destructured-binding-property-name.ts
4880
4948
  const getDestructuredBindingPropertyName = (bindingIdentifier) => {
4881
4949
  let bindingNode = bindingIdentifier;
@@ -8940,65 +9008,6 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
8940
9008
  }
8941
9009
  };
8942
9010
  //#endregion
8943
- //#region src/plugin/utils/is-react-api-call.ts
8944
- const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
8945
- const isImportedFromReact = (symbol) => {
8946
- if (symbol.kind !== "import") return false;
8947
- const importDeclaration = symbol.declarationNode.parent;
8948
- return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
8949
- };
8950
- const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
8951
- if (!isNodeOfType(identifier, "Identifier")) return false;
8952
- const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
8953
- if (!symbol || !isImportedFromReact(symbol)) return false;
8954
- const importedName = getImportedName(symbol.declarationNode);
8955
- return Boolean(importedName && includesApiName(apiNames, importedName));
8956
- };
8957
- const isReactNamespaceImport = (identifier, scopes) => {
8958
- const symbol = resolveConstIdentifierAlias(identifier, scopes);
8959
- if (!symbol || !isImportedFromReact(symbol)) return false;
8960
- return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
8961
- };
8962
- const isReactNamespaceReceiver = (receiver, scopes, options) => {
8963
- if (!isNodeOfType(receiver, "Identifier")) return false;
8964
- if (isReactNamespaceImport(receiver, scopes)) return true;
8965
- return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
8966
- };
8967
- const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
8968
- const symbol = scopes.symbolFor(identifier);
8969
- if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
8970
- const pattern = symbol.declarationNode.id;
8971
- if (!isNodeOfType(pattern, "ObjectPattern")) return false;
8972
- for (const property of pattern.properties) {
8973
- if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
8974
- const propertyName = getStaticPropertyKeyName(property);
8975
- return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver(stripParenExpression(symbol.initializer), scopes, options));
8976
- }
8977
- return false;
8978
- };
8979
- const isReactApiCall = (node, apiNames, scopes, options = {}) => {
8980
- if (!isNodeOfType(node, "CallExpression")) return false;
8981
- return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
8982
- };
8983
- const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
8984
- const callee = stripParenExpression(rawCallee);
8985
- if (options.resolveConditionalAliases && isNodeOfType(callee, "ConditionalExpression")) return isReactApiCallee(callee.consequent, apiNames, scopes, options, new Set(visitedSymbolIds)) && isReactApiCallee(callee.alternate, apiNames, scopes, options, new Set(visitedSymbolIds));
8986
- if (isNodeOfType(callee, "Identifier")) {
8987
- if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
8988
- if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
8989
- if (options.resolveConditionalAliases) {
8990
- const symbol = scopes.symbolFor(callee);
8991
- if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
8992
- visitedSymbolIds.add(symbol.id);
8993
- return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
8994
- }
8995
- }
8996
- return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
8997
- }
8998
- if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
8999
- return isReactNamespaceReceiver(stripParenExpression(callee.object), scopes, options);
9000
- };
9001
- //#endregion
9002
9011
  //#region src/plugin/utils/is-proven-browser-api-receiver.ts
9003
9012
  const DOM_EVENT_TARGET_TYPE_NAMES = new Set([
9004
9013
  "AbortSignal",
@@ -13652,7 +13661,7 @@ const getPromiseChainCallForCallback = (candidate) => {
13652
13661
  if (!callbackContainer.arguments?.some((argument) => stripParenExpression(argument) === candidate)) return null;
13653
13662
  return isPromiseChainCall(stripParenExpression(callbackContainer.callee)) ? callbackContainer : null;
13654
13663
  };
13655
- const collectEffectInvokedFunctions = (effectCallback) => {
13664
+ const collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
13656
13665
  const invokedFunctions = new Set([effectCallback]);
13657
13666
  const localFunctionBindings = /* @__PURE__ */ new Map();
13658
13667
  const calledBindingNames = /* @__PURE__ */ new Set();
@@ -13686,12 +13695,14 @@ const collectEffectInvokedFunctions = (effectCallback) => {
13686
13695
  calledBindingNames.add(callee.name);
13687
13696
  return;
13688
13697
  }
13689
- if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
13698
+ if (includePromiseCallbacks && isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
13690
13699
  });
13691
13700
  for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
13692
13701
  }
13693
13702
  return invokedFunctions;
13694
13703
  };
13704
+ const collectEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, true);
13705
+ const collectSynchronouslyEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, false);
13695
13706
  //#endregion
13696
13707
  //#region src/plugin/utils/is-react-hook-name.ts
13697
13708
  const isReactHookName = (name) => {
@@ -13850,15 +13861,19 @@ const resolveReactRefSymbol = (memberExpression, scopes) => {
13850
13861
  if (!isNodeOfType(initializer, "CallExpression")) return null;
13851
13862
  return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
13852
13863
  };
13853
- const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
13864
+ const resolveReactRefCurrentOriginSymbol = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
13854
13865
  const expression = stripParenExpression(node);
13855
- if (resolveReactRefSymbol(expression, scopes)) return true;
13856
- if (!isNodeOfType(expression, "Identifier")) return false;
13857
- const symbol = resolveConstIdentifierAlias(expression, scopes);
13858
- if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
13866
+ const refSymbol = resolveReactRefSymbol(expression, scopes);
13867
+ if (refSymbol) return refSymbol;
13868
+ if (!isNodeOfType(expression, "Identifier")) return null;
13869
+ const symbol = scopes.symbolFor(expression);
13870
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
13871
+ const initializer = getDirectUnreassignedInitializer(symbol);
13872
+ if (!initializer) return null;
13859
13873
  visitedSymbolIds.add(symbol.id);
13860
- return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
13874
+ return resolveReactRefCurrentOriginSymbol(initializer, scopes, visitedSymbolIds);
13861
13875
  };
13876
+ const hasReactRefCurrentOrigin = (node, scopes) => resolveReactRefCurrentOriginSymbol(node, scopes) !== null;
13862
13877
  //#endregion
13863
13878
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
13864
13879
  const walkInsideStatementBlocks = (node, visitor) => {
@@ -13876,6 +13891,7 @@ const walkInsideStatementBlocks = (node, visitor) => {
13876
13891
  };
13877
13892
  //#endregion
13878
13893
  //#region src/plugin/rules/state-and-effects/utils/is-subscribe-like-call-expression.ts
13894
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
13879
13895
  const getSubscribeLikeMethodName = (node) => {
13880
13896
  if (!isNodeOfType(node, "CallExpression")) return null;
13881
13897
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
@@ -13886,6 +13902,11 @@ const isSubscribeLikeCallExpression = (node) => {
13886
13902
  const methodName = getSubscribeLikeMethodName(node);
13887
13903
  return methodName !== null && SUBSCRIPTION_METHOD_NAMES.has(methodName);
13888
13904
  };
13905
+ const getSubscribeOrObserveMethodName = (node) => {
13906
+ const methodName = getSubscribeLikeMethodName(node);
13907
+ return methodName !== null && (SUBSCRIPTION_METHOD_NAMES.has(methodName) || methodName === "observe") ? methodName : null;
13908
+ };
13909
+ const isSubscribeOrObserveCallExpression = (node) => getSubscribeOrObserveMethodName(node) !== null;
13889
13910
  const isCleanupReturningSubscribeLikeCallExpression = (node) => {
13890
13911
  const methodName = getSubscribeLikeMethodName(node);
13891
13912
  if (methodName === null || !CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES.has(methodName)) return false;
@@ -13938,7 +13959,6 @@ const isNodeReachableWithinFunction = (node, context) => {
13938
13959
  };
13939
13960
  //#endregion
13940
13961
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
13941
- const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
13942
13962
  const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
13943
13963
  const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
13944
13964
  const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
@@ -13948,10 +13968,6 @@ const RESOURCE_NOUN_BY_KIND = {
13948
13968
  socket: "connection"
13949
13969
  };
13950
13970
  const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
13951
- const isSubscribeOrObserveCall = (node) => {
13952
- if (isSubscribeLikeCallExpression(node)) return true;
13953
- return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
13954
- };
13955
13971
  const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
13956
13972
  if (!expression) return null;
13957
13973
  const unwrappedExpression = stripParenExpression(expression);
@@ -14053,12 +14069,13 @@ const findSubscribeLikeUsages = (callback, context) => {
14053
14069
  });
14054
14070
  return;
14055
14071
  }
14056
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) {
14072
+ const subscribeOrObserveMethodName = getSubscribeOrObserveMethodName(child);
14073
+ if (subscribeOrObserveMethodName !== null) {
14057
14074
  const registrationDetails = getCallRegistrationDetails(child, context);
14058
14075
  usages.push({
14059
14076
  kind: "subscribe",
14060
14077
  node: child,
14061
- resourceName: child.callee.property.name,
14078
+ resourceName: subscribeOrObserveMethodName,
14062
14079
  handleKey: findAssignedResourceKey(child, context),
14063
14080
  ...registrationDetails
14064
14081
  });
@@ -14340,6 +14357,39 @@ const findContainingCollectionKey = (resourceNode, context) => {
14340
14357
  }
14341
14358
  return null;
14342
14359
  };
14360
+ const findPushedResourceCollectionKey = (usage, context) => {
14361
+ if (!isNodeOfType(usage.node, "CallExpression")) return null;
14362
+ const registrationCallee = stripParenExpression(usage.node.callee);
14363
+ if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed) return null;
14364
+ const resourceIdentifier = stripParenExpression(registrationCallee.object);
14365
+ if (!isPrivatePlainConstIdentifier(resourceIdentifier, context)) return null;
14366
+ const resourceSymbol = context.scopes.symbolFor(resourceIdentifier);
14367
+ if (!resourceSymbol) return null;
14368
+ const pushCalls = resourceSymbol.references.flatMap((reference) => {
14369
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
14370
+ const callNode = referenceRoot.parent;
14371
+ if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((argument) => argument === referenceRoot)) return [];
14372
+ const pushCallee = stripParenExpression(callNode.callee);
14373
+ return isNodeOfType(pushCallee, "MemberExpression") && !pushCallee.computed && isNodeOfType(pushCallee.object, "Identifier") && isNodeOfType(pushCallee.property, "Identifier") && pushCallee.property.name === "push" ? [callNode] : [];
14374
+ });
14375
+ if (pushCalls.length !== 1) return null;
14376
+ const pushCall = pushCalls[0];
14377
+ if (findEnclosingFunction$1(pushCall) !== findEnclosingFunction$1(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [pushCall], context)) return null;
14378
+ const pushCallee = stripParenExpression(pushCall.callee);
14379
+ if (!isNodeOfType(pushCallee, "MemberExpression") || !isNodeOfType(pushCallee.object, "Identifier") || !isPrivatePlainConstIdentifier(pushCallee.object, context)) return null;
14380
+ const collectionSymbol = context.scopes.symbolFor(pushCallee.object);
14381
+ const collectionInitializer = collectionSymbol?.initializer ? stripParenExpression(collectionSymbol.initializer) : null;
14382
+ if (!collectionSymbol || !isNodeOfType(collectionInitializer, "ArrayExpression") || (collectionInitializer.elements?.length ?? 0) !== 0 || findEnclosingFunction$1(collectionSymbol.declarationNode) !== findEnclosingFunction$1(usage.node)) return null;
14383
+ return collectionSymbol.references.every((reference) => {
14384
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
14385
+ const forOfStatement = referenceRoot.parent;
14386
+ if (isNodeOfType(forOfStatement, "ForOfStatement") && forOfStatement.right === referenceRoot && forOfStatement.await !== true) return true;
14387
+ const memberNode = referenceRoot.parent;
14388
+ const callNode = memberNode?.parent;
14389
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== referenceRoot || memberNode.computed || !isNodeOfType(memberNode.property, "Identifier") || !isNodeOfType(callNode, "CallExpression") || callNode.callee !== memberNode) return false;
14390
+ return memberNode.property.name === "forEach" || memberNode.property.name === "push";
14391
+ }) ? resolveExpressionKey(pushCallee.object, context) : null;
14392
+ };
14343
14393
  const isWithinAssignmentTarget = (identifier) => {
14344
14394
  let currentNode = identifier;
14345
14395
  let parentNode = currentNode.parent;
@@ -14398,6 +14448,14 @@ const isSynchronousIteratorCallback = (functionNode) => {
14398
14448
  if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
14399
14449
  return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
14400
14450
  };
14451
+ const findEnclosingForEachCall = (node) => {
14452
+ const callbackNode = findEnclosingFunction$1(node);
14453
+ if (!callbackNode) return null;
14454
+ const callNode = callbackNode.parent;
14455
+ if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
14456
+ const callee = stripParenExpression(callNode.callee);
14457
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" ? callNode : null;
14458
+ };
14401
14459
  const findDirectCallForReference = (identifier) => {
14402
14460
  const expressionRoot = findTransparentExpressionRoot(identifier);
14403
14461
  const callNode = expressionRoot.parent;
@@ -14412,7 +14470,7 @@ const findSingleDirectInvocation = (functionNode, caller, context) => {
14412
14470
  const callNode = findDirectCallForReference(reference.identifier);
14413
14471
  return callNode ? [callNode] : [];
14414
14472
  });
14415
- if (invocationCalls.length !== 1) return null;
14473
+ if (invocationCalls.length !== 1 || symbol.references.length !== 1) return null;
14416
14474
  const invocationCall = invocationCalls[0];
14417
14475
  return findEnclosingFunction$1(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
14418
14476
  };
@@ -14446,7 +14504,15 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
14446
14504
  if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
14447
14505
  const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
14448
14506
  if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
14449
- const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context);
14507
+ const cleanupForEachCall = findEnclosingForEachCall(cleanupChild);
14508
+ const cleanupCallee = isNodeOfType(cleanupCall, "CallExpression") ? stripParenExpression(cleanupCall.callee) : null;
14509
+ const cleanupReceiverForOfStatement = isNodeOfType(cleanupCallee, "MemberExpression") ? findForOfStatementForIteratorExpression(cleanupCallee.object, context) : null;
14510
+ const cleanupReceiverCollectionKey = cleanupReceiverForOfStatement ? resolveExpressionKey(cleanupReceiverForOfStatement.right, context) : isNodeOfType(cleanupCallee, "MemberExpression") ? resolveIteratorCollectionKey(cleanupCallee.object, context) : null;
14511
+ if (cleanupReceiverCollectionKey !== null && findEnclosingFunction$1(cleanupChild) !== cleanupFunction) {
14512
+ if (cleanupForEachCall && findPushedResourceCollectionKey(usage, context) === cleanupReceiverCollectionKey) matchingLoopOrHelperAnchors.push(cleanupForEachCall);
14513
+ return;
14514
+ }
14515
+ const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context) ?? cleanupReceiverForOfStatement;
14450
14516
  if (!cleanupForOfStatement) {
14451
14517
  didCleanupFunctionMatch = true;
14452
14518
  return false;
@@ -14490,28 +14556,28 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
14490
14556
  });
14491
14557
  return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
14492
14558
  };
14493
- const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => {
14494
- if (usage.handleKey === null) return null;
14495
- const doesTestRequireLiveHandle = (test) => {
14496
- if (resolveExpressionKey(test, context) === usage.handleKey) return true;
14497
- const unwrappedTest = stripParenExpression(test);
14498
- if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
14499
- const isNullishOperand = (operand) => {
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);
14559
+ const doesTestRequireLiveExpressionKey = (test, expressionKey, context) => {
14560
+ if (resolveExpressionKey(test, context) === expressionKey) return true;
14561
+ const unwrappedTest = stripParenExpression(test);
14562
+ if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
14563
+ const isNullishOperand = (operand) => {
14564
+ const unwrappedOperand = stripParenExpression(operand);
14565
+ return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
14504
14566
  };
14567
+ return resolveExpressionKey(unwrappedTest.left, context) === expressionKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === expressionKey && isNullishOperand(unwrappedTest.left);
14568
+ };
14569
+ const findLiveExpressionGuardForRelease = (releaseCall, owner, expressionKey, context) => {
14505
14570
  let ancestor = releaseCall.parent;
14506
14571
  while (ancestor && ancestor !== owner) {
14507
14572
  if (isNodeOfType(ancestor, "IfStatement")) {
14508
- if (ancestor.alternate !== null || !doesTestRequireLiveHandle(ancestor.test) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
14573
+ if (ancestor.alternate !== null || !doesTestRequireLiveExpressionKey(ancestor.test, expressionKey, context) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
14509
14574
  return ancestor;
14510
14575
  }
14511
14576
  ancestor = ancestor.parent;
14512
14577
  }
14513
14578
  return null;
14514
14579
  };
14580
+ const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => usage.handleKey === null ? null : findLiveExpressionGuardForRelease(releaseCall, owner, usage.handleKey, context);
14515
14581
  const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
14516
14582
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
14517
14583
  const functionCfg = context.cfg.cfgFor(callback);
@@ -14540,7 +14606,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
14540
14606
  walkAst(componentFunction.body, (child) => {
14541
14607
  if (didFindUnmountCleanup) return false;
14542
14608
  if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction) return;
14543
- if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
14609
+ if (!isReactHookCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
14544
14610
  const dependencyList = child.arguments?.[1];
14545
14611
  if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
14546
14612
  const cleanupCallback = getEffectCallback(child);
@@ -14699,7 +14765,108 @@ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, con
14699
14765
  });
14700
14766
  return hasPotentialInterruption;
14701
14767
  };
14768
+ const getNumericReactRefCurrentKey = (expression, context) => {
14769
+ const refSymbol = resolveReactRefSymbol(stripParenExpression(expression), context.scopes);
14770
+ const initializer = refSymbol?.initializer ? stripParenExpression(refSymbol.initializer) : null;
14771
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
14772
+ const initialValue = initializer.arguments?.[0] ? stripParenExpression(initializer.arguments[0]) : null;
14773
+ if (!isNodeOfType(initialValue, "Literal") || typeof initialValue.value !== "number") return null;
14774
+ return resolveExpressionKey(expression, context);
14775
+ };
14776
+ const getBlockingGenerationKey = (expression, context) => {
14777
+ const test = stripParenExpression(expression);
14778
+ if (isNodeOfType(test, "LogicalExpression") && test.operator === "||") return getBlockingGenerationKey(test.left, context) ?? getBlockingGenerationKey(test.right, context);
14779
+ if (!isNodeOfType(test, "BinaryExpression") || test.operator !== "!==" && test.operator !== "!=") return null;
14780
+ const leftKey = getNumericReactRefCurrentKey(test.left, context);
14781
+ const rightKey = getNumericReactRefCurrentKey(test.right, context);
14782
+ const snapshotExpression = leftKey ? stripParenExpression(test.right) : stripParenExpression(test.left);
14783
+ const key = leftKey ?? rightKey;
14784
+ return key && isNodeOfType(snapshotExpression, "Identifier") ? key : null;
14785
+ };
14786
+ const findGenerationGuardKeyForDeferredUsage = (usageFunction, usageNode, context) => {
14787
+ if (!isFunctionLike$1(usageFunction)) return null;
14788
+ let generationKey = null;
14789
+ walkAst(usageFunction.body, (child) => {
14790
+ if (generationKey) return false;
14791
+ if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
14792
+ if (!isNodeOfType(child, "IfStatement") || child.alternate) return;
14793
+ const key = getBlockingGenerationKey(child.test, context);
14794
+ if (!key || canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, usageFunction, context) || !doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], usageFunction, context)) return;
14795
+ generationKey = key;
14796
+ });
14797
+ return generationKey;
14798
+ };
14799
+ const isGenerationAdvance = (node, generationKey, context) => {
14800
+ if (isNodeOfType(node, "UpdateExpression") && resolveExpressionKey(node.argument, context) === generationKey) return true;
14801
+ if (!isNodeOfType(node, "AssignmentExpression") || resolveExpressionKey(node.left, context) !== generationKey || node.operator !== "+=" && node.operator !== "-=") return false;
14802
+ const amount = stripParenExpression(node.right);
14803
+ return isNodeOfType(amount, "Literal") && typeof amount.value === "number" && amount.value !== 0;
14804
+ };
14805
+ const functionAdvancesGeneration = (owner, generationKey, context) => {
14806
+ if (!isFunctionLike$1(owner)) return false;
14807
+ let didAdvanceGeneration = false;
14808
+ walkAst(owner.body, (child) => {
14809
+ if (didAdvanceGeneration) return false;
14810
+ if (child !== owner.body && isFunctionLike$1(child)) return false;
14811
+ if (isGenerationAdvance(child, generationKey, context)) {
14812
+ didAdvanceGeneration = true;
14813
+ return false;
14814
+ }
14815
+ });
14816
+ return didAdvanceGeneration;
14817
+ };
14818
+ const cleanupReturnsReleaseUsage = (cleanupReturns, usage, context) => cleanupReturns.length > 0 && cleanupReturns.every((cleanupReturn) => {
14819
+ if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
14820
+ const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
14821
+ return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
14822
+ });
14823
+ const getOwnedFunctionReference = (reference, usageFunction, usageNode, callback, cleanupReturns, context) => {
14824
+ const directCall = findDirectCallForReference(reference);
14825
+ if (directCall) {
14826
+ const referenceOwner = findEnclosingFunction$1(directCall);
14827
+ if (referenceOwner && referenceOwner !== usageFunction && collectSynchronouslyEffectInvokedFunctions(callback).has(referenceOwner)) return { generationKey: null };
14828
+ const generationKey = referenceOwner ? findGenerationGuardKeyForDeferredUsage(referenceOwner, directCall, context) : null;
14829
+ return generationKey ? { generationKey } : null;
14830
+ }
14831
+ const referenceRoot = findTransparentExpressionRoot(reference);
14832
+ const schedulerCall = referenceRoot.parent;
14833
+ 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;
14834
+ const schedulerUsage = {
14835
+ kind: "timer",
14836
+ node: schedulerCall,
14837
+ resourceName: schedulerCall.callee.name,
14838
+ handleKey: findAssignedResourceKey(schedulerCall, context),
14839
+ receiverKey: null,
14840
+ registrationVerbName: schedulerCall.callee.name,
14841
+ eventKey: null,
14842
+ handlerKey: null
14843
+ };
14844
+ const generationKey = findGenerationGuardKeyForDeferredUsage(usageFunction, usageNode, context);
14845
+ return schedulerUsage.handleKey !== null && cleanupReturnsReleaseUsage(cleanupReturns, schedulerUsage, context) && generationKey ? { generationKey } : null;
14846
+ };
14847
+ const hasGuardedRefOwnedNestedCleanup = (callback, usage, cleanupReturns, context) => {
14848
+ const usageFunction = findEnclosingFunction$1(usage.node);
14849
+ const usageExpression = findTransparentExpressionRoot(usage.node);
14850
+ const usageAssignment = usageExpression.parent;
14851
+ 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;
14852
+ const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
14853
+ if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
14854
+ const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
14855
+ return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
14856
+ });
14857
+ const bindingIdentifier = getFunctionBindingIdentifier$1(usageFunction);
14858
+ const functionSymbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
14859
+ if (!functionSymbol || functionSymbol.references.length === 0) return false;
14860
+ const ownedReferences = functionSymbol.references.map((reference) => getOwnedFunctionReference(reference.identifier, usageFunction, usage.node, callback, cleanupReturns, context));
14861
+ if (ownedReferences.some((reference) => reference === null)) return false;
14862
+ const generationKeys = new Set(ownedReferences.flatMap((reference) => reference?.generationKey ? [reference.generationKey] : []));
14863
+ if (generationKeys.size !== 1) return false;
14864
+ const generationKey = generationKeys.values().next().value;
14865
+ if (typeof generationKey !== "string") return false;
14866
+ return [...collectSynchronouslyEffectInvokedFunctions(callback), ...cleanupFunctions].some((owner) => functionAdvancesGeneration(owner, generationKey, context));
14867
+ };
14702
14868
  const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
14869
+ if (hasGuardedRefOwnedNestedCleanup(callback, usage, cleanupReturns, context)) return true;
14703
14870
  const usageFunction = findEnclosingFunction$1(usage.node);
14704
14871
  const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
14705
14872
  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 +15037,7 @@ const getReleaseVerbName = (node) => {
14870
15037
  const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
14871
15038
  const releaseFunction = findEnclosingFunction$1(releaseReceiver);
14872
15039
  const usageFunction = findEnclosingFunction$1(usage.node);
14873
- if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !hasReactRefCurrentOrigin(releaseReceiver, context.scopes)) return false;
15040
+ if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction, context) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
14874
15041
  const controllerKey = getListenerAbortControllerKey(usage, context);
14875
15042
  const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
14876
15043
  if (controllerKey === null || refCurrentKey === null) return false;
@@ -14890,6 +15057,62 @@ const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) =>
14890
15057
  const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
14891
15058
  return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
14892
15059
  };
15060
+ const isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
15061
+ const isFunctionForwardedToReactRef = (functionNode, context) => {
15062
+ const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
15063
+ if (!bindingIdentifier) return false;
15064
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
15065
+ if (!symbol) return false;
15066
+ return symbol.references.some((reference) => {
15067
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
15068
+ const expressionContainer = referenceRoot.parent;
15069
+ return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
15070
+ });
15071
+ };
15072
+ const isFunctionReturnedFromReactHook = (functionNode, context, requireRefPropertyName) => {
15073
+ const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
15074
+ if (!bindingIdentifier) return false;
15075
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
15076
+ if (!symbol) return false;
15077
+ return symbol.references.some((reference) => {
15078
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
15079
+ const property = referenceRoot.parent;
15080
+ const propertyName = isNodeOfType(property, "Property") ? getStaticPropertyKeyName(property) : null;
15081
+ if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression") || requireRefPropertyName && propertyName !== "ref" && !propertyName?.endsWith("Ref")) return false;
15082
+ const returnedObject = findTransparentExpressionRoot(property.parent);
15083
+ const returnStatement = returnedObject.parent;
15084
+ if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
15085
+ const ownerFunction = findEnclosingFunction$1(returnStatement);
15086
+ return Boolean(ownerFunction && isReactHookName(getFunctionBindingIdentifier$1(ownerFunction)?.name ?? ""));
15087
+ });
15088
+ };
15089
+ const isFunctionUsedAsReactRef = (functionNode, context) => isFunctionForwardedToReactRef(functionNode, context) || isFunctionReturnedFromReactHook(functionNode, context, true);
15090
+ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
15091
+ if (!isNodeOfType(usage.node, "CallExpression")) return false;
15092
+ const usageFunction = findEnclosingFunction$1(usage.node);
15093
+ if (!usageFunction || !isFunctionLike$1(usageFunction) || usageFunction !== findEnclosingFunction$1(releaseCall) || !isFunctionUsedAsReactRef(usageFunction, context)) return false;
15094
+ const registrationCallee = stripParenExpression(usage.node.callee);
15095
+ const releaseCallee = stripParenExpression(releaseCall.callee);
15096
+ const releaseRefSymbol = isNodeOfType(releaseCallee, "MemberExpression") ? resolveReactRefCurrentOriginSymbol(releaseCallee.object, context.scopes) : null;
15097
+ 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;
15098
+ const registrationReceiverKey = resolveExpressionKey(stripParenExpression(registrationCallee.object), context);
15099
+ const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
15100
+ const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
15101
+ 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;
15102
+ const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15103
+ const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
15104
+ if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15105
+ const releaseStart = getRangeStart(releaseCall);
15106
+ const matchingOwnershipAssignments = [];
15107
+ const usageFunctionBody = usageFunction.body;
15108
+ walkAst(usageFunctionBody, (child) => {
15109
+ if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
15110
+ 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);
15111
+ });
15112
+ const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? releaseCall;
15113
+ const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
15114
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
15115
+ };
14893
15116
  const doesReleaseCallMatchUsage = (node, usage, context) => {
14894
15117
  const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
14895
15118
  if (!isNodeOfType(callNode, "CallExpression")) return false;
@@ -14906,14 +15129,21 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14906
15129
  if (!releaseVerbName) return false;
14907
15130
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
14908
15131
  const releaseReceiverKey = resolveExpressionKey(callee.object, context);
15132
+ const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
15133
+ const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
15134
+ const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
15135
+ const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
15136
+ const releaseReceiverCollectionKey = releaseReceiverForOfStatement ? resolveExpressionKey(releaseReceiverForOfStatement.right, context) : resolveIteratorCollectionKey(callee.object, context);
15137
+ if (pairedReleaseVerbNames && matchesPairedReleaseVerb(releaseVerbName, pairedReleaseVerbNames) && pushedResourceCollectionKey !== null && pushedResourceCollectionKey === releaseReceiverCollectionKey && (releaseVerbName !== "unobserve" || usage.eventKey !== null && releaseEventKey === usage.eventKey)) return true;
15138
+ if (isReactRefListenerReplacementRelease(callNode, usage, context)) return true;
14909
15139
  if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
14910
15140
  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
15141
  if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
14912
15142
  if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
14913
15143
  if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
15144
+ if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
14914
15145
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
14915
15146
  if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
14916
- const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
14917
15147
  const usageEventArgument = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[0] : null;
14918
15148
  const releaseEventArgument = callNode.arguments?.[0];
14919
15149
  if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
@@ -14947,13 +15177,14 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
14947
15177
  const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
14948
15178
  if (!releaseHandler) return releaseVerbName === "off";
14949
15179
  const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
14950
- return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
15180
+ const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
15181
+ return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
14951
15182
  }
14952
15183
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
14953
15184
  return true;
14954
15185
  };
14955
15186
  const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
14956
- const isReturnedEffectCleanupFunction = (functionNode) => {
15187
+ const isReturnedEffectCleanupFunction = (functionNode, context) => {
14957
15188
  let currentNode = functionNode;
14958
15189
  let parentNode = currentNode.parent;
14959
15190
  while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
@@ -14962,23 +15193,175 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
14962
15193
  }
14963
15194
  const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
14964
15195
  const effectCall = effectCallback?.parent;
14965
- return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
15196
+ return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes));
14966
15197
  };
14967
15198
  const isPotentiallyReachableFunction = (functionNode, context) => {
14968
- if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
15199
+ if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
14969
15200
  const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
14970
15201
  if (!bindingIdentifier) return false;
14971
15202
  const symbol = context.scopes.symbolFor(bindingIdentifier);
14972
15203
  if (!symbol) return false;
14973
15204
  return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
14974
15205
  };
15206
+ const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
15207
+ if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
15208
+ const usageFunction = findEnclosingFunction$1(usage.node);
15209
+ if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
15210
+ const assignments = /* @__PURE__ */ new Map();
15211
+ const collectAssignment = (expression) => {
15212
+ const expressionRoot = findTransparentExpressionRoot(expression);
15213
+ const assignment = expressionRoot.parent;
15214
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
15215
+ const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
15216
+ const refCurrentKey = resolveExpressionKey(assignment.left, context);
15217
+ const retainedFunction = findEnclosingFunction$1(assignment);
15218
+ const assignmentStart = getRangeStart(assignment);
15219
+ if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
15220
+ assignments.set(assignmentStart, {
15221
+ assignmentNode: assignment,
15222
+ refCurrentKey,
15223
+ retainedFunction
15224
+ });
15225
+ };
15226
+ collectAssignment(disposerFunction);
15227
+ const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
15228
+ const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
15229
+ for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
15230
+ walkAst(usageFunction.body, (child) => {
15231
+ if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
15232
+ if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
15233
+ });
15234
+ return [...assignments.values()];
15235
+ };
15236
+ const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
15237
+ const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
15238
+ let hasUnsafeOverwrite = false;
15239
+ walkAst(storage.retainedFunction.body, (child) => {
15240
+ if (hasUnsafeOverwrite) return false;
15241
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15242
+ if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
15243
+ const storedValue = resolveStableValue(child.right, context);
15244
+ if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
15245
+ hasUnsafeOverwrite = true;
15246
+ return false;
15247
+ }
15248
+ });
15249
+ return hasUnsafeOverwrite;
15250
+ };
15251
+ const hasEffectCleanupInvocation = (storage, usage, context) => {
15252
+ const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
15253
+ if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
15254
+ const cleanupFunctionInvokesRef = (cleanupFunction) => {
15255
+ if (!isFunctionLike$1(cleanupFunction)) return false;
15256
+ let didFindCleanupCall = false;
15257
+ walkAst(cleanupFunction.body, (child) => {
15258
+ if (didFindCleanupCall) return false;
15259
+ if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
15260
+ if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
15261
+ const callRoot = findTransparentExpressionRoot(child);
15262
+ const callStatement = callRoot.parent;
15263
+ const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
15264
+ const isConciseBody = cleanupFunction.body === callRoot;
15265
+ if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
15266
+ didFindCleanupCall = true;
15267
+ return false;
15268
+ }
15269
+ }
15270
+ });
15271
+ return didFindCleanupCall;
15272
+ };
15273
+ const effectReturnsCleanup = (effectCallback) => {
15274
+ if (!isFunctionLike$1(effectCallback)) return false;
15275
+ if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
15276
+ const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
15277
+ return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
15278
+ }
15279
+ const matchingReturns = [];
15280
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
15281
+ if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
15282
+ const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
15283
+ if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
15284
+ matchingReturns.push(child);
15285
+ });
15286
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
15287
+ };
15288
+ let didFindInvocation = false;
15289
+ walkAst(componentFunction.body, (child) => {
15290
+ if (didFindInvocation) return false;
15291
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
15292
+ const effectCallback = getEffectCallback(child);
15293
+ if (effectCallback && effectReturnsCleanup(effectCallback)) {
15294
+ didFindInvocation = true;
15295
+ return false;
15296
+ }
15297
+ });
15298
+ return didFindInvocation;
15299
+ };
15300
+ const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
15301
+ const isReturnedCallbackRefShape = () => {
15302
+ if (!isFunctionLike$1(storage.retainedFunction)) return false;
15303
+ const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
15304
+ if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
15305
+ const nodeParameter = storage.retainedFunction.params?.[0];
15306
+ const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
15307
+ if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
15308
+ if (!isFunctionReturnedFromReactHook(storage.retainedFunction, context, false)) return false;
15309
+ const usageStart = getRangeStart(usage.node);
15310
+ if (usageStart === null) return false;
15311
+ let hasNullExit = false;
15312
+ walkAst(storage.retainedFunction.body, (child) => {
15313
+ if (hasNullExit) return false;
15314
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15315
+ if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
15316
+ const test = stripParenExpression(child.test);
15317
+ if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
15318
+ const consequent = child.consequent;
15319
+ hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
15320
+ if (hasNullExit) return false;
15321
+ });
15322
+ return hasNullExit;
15323
+ };
15324
+ if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
15325
+ const cleanupCalls = [];
15326
+ walkAst(storage.retainedFunction.body, (child) => {
15327
+ if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
15328
+ if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
15329
+ });
15330
+ return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
15331
+ };
15332
+ const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
15333
+ const disposerFunction = findEnclosingFunction$1(releaseNode);
15334
+ if (!disposerFunction) return false;
15335
+ return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
15336
+ };
15337
+ const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
15338
+ 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;
15339
+ const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15340
+ const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
15341
+ if (!isNodeOfType(releaseCall, "CallExpression")) return false;
15342
+ const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
15343
+ if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15344
+ const ownerFunction = findEnclosingFunction$1(releaseFunction);
15345
+ if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
15346
+ const triggerRegistrations = [];
15347
+ walkAst(ownerFunction.body, (child) => {
15348
+ if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
15349
+ if (!isNodeOfType(child, "CallExpression")) return;
15350
+ const registrationDetails = getCallRegistrationDetails(child, context);
15351
+ if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
15352
+ });
15353
+ if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
15354
+ return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
15355
+ };
14975
15356
  const isReleaseReachableForUsage = (releaseNode, usage, context) => {
14976
15357
  if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
14977
15358
  const releaseFunction = findEnclosingFunction$1(releaseNode);
14978
15359
  if (!releaseFunction) return true;
14979
15360
  if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
15361
+ if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
14980
15362
  const usageFunction = findEnclosingFunction$1(usage.node);
14981
15363
  if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
15364
+ if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
14982
15365
  return isPotentiallyReachableFunction(releaseFunction, context);
14983
15366
  };
14984
15367
  const fileContainsReleaseForUsage = (usage, context) => {
@@ -15310,7 +15693,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
15310
15693
  return false;
15311
15694
  }
15312
15695
  }
15313
- if (isSubscribeOrObserveCall(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
15696
+ if (isSubscribeOrObserveCallExpression(child) && (!doesResourceResultEscape(child, allowReturnedResourceEscape, allowReturnedResourceEscape, context) || options?.requireCallableReturnedResource === true && !isCleanupReturningSubscribeLikeCallExpression(child))) {
15314
15697
  const registrationDetails = getCallRegistrationDetails(child, context);
15315
15698
  const subscriptionUsage = {
15316
15699
  kind: "subscribe",
@@ -15518,7 +15901,7 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
15518
15901
  if (!isFunctionLike$1(functionNode)) return false;
15519
15902
  const functionRoot = findTransparentExpressionRoot(functionNode);
15520
15903
  const callbackCall = functionRoot.parent;
15521
- if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
15904
+ if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isReactHookCall(callbackCall, "useCallback", context.scopes) && isDirectJsxEventHandlerValue(callbackCall)) return true;
15522
15905
  const parentNode = functionNode.parent;
15523
15906
  if (isDirectJsxEventHandlerValue(functionNode)) return true;
15524
15907
  if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
@@ -15554,12 +15937,12 @@ const effectNeedsCleanup = defineRule({
15554
15937
  };
15555
15938
  return {
15556
15939
  CallExpression(node) {
15557
- if (isHookCall$2(node, "useCallback")) {
15940
+ if (isReactHookCall(node, "useCallback", context.scopes)) {
15558
15941
  const retainedCallback = getEffectCallback(node);
15559
15942
  if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
15560
15943
  return;
15561
15944
  }
15562
- if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
15945
+ if (!isReactHookCall(node, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
15563
15946
  const callback = getEffectCallback(node);
15564
15947
  if (!callback) return;
15565
15948
  const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
@@ -15567,7 +15950,7 @@ const effectNeedsCleanup = defineRule({
15567
15950
  const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
15568
15951
  if (!firstUsage) return;
15569
15952
  const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
15570
- const hookName = getCalleeName$2(node) ?? "effect";
15953
+ const hookName = getCalleeName$1(node) ?? "effect";
15571
15954
  context.report({
15572
15955
  node,
15573
15956
  message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without guaranteed cleanup. Return a cleanup function that owns every allocation so it does not leak after unmount.`
@@ -16910,7 +17293,38 @@ const symbolHasStableImportedAlias = (symbol, scopes) => {
16910
17293
  const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
16911
17294
  return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
16912
17295
  };
16913
- const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
17296
+ const isAssignmentTarget = (node) => {
17297
+ let currentNode = findTransparentExpressionRoot(node);
17298
+ while (currentNode.parent) {
17299
+ const parentNode = currentNode.parent;
17300
+ if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
17301
+ if (isNodeOfType(parentNode, "UpdateExpression")) return parentNode.argument === currentNode;
17302
+ if (isNodeOfType(parentNode, "UnaryExpression")) return parentNode.operator === "delete" && parentNode.argument === currentNode;
17303
+ if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
17304
+ if (isNodeOfType(parentNode, "ArrayPattern") && parentNode.elements.some((element) => element === currentNode) || isNodeOfType(parentNode, "ObjectPattern") && parentNode.properties.some((property) => property === currentNode) || isNodeOfType(parentNode, "RestElement") && parentNode.argument === currentNode || isNodeOfType(parentNode, "AssignmentPattern") && parentNode.left === currentNode || isNodeOfType(parentNode, "Property") && parentNode.value === currentNode && isNodeOfType(parentNode.parent, "ObjectPattern")) {
17305
+ currentNode = parentNode;
17306
+ continue;
17307
+ }
17308
+ return false;
17309
+ }
17310
+ return false;
17311
+ };
17312
+ const symbolHasStableRefLazyInitialization = (symbol, scopes) => {
17313
+ if (symbol.kind !== "const" || symbol.references.some((reference) => reference.flag !== "read")) return false;
17314
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
17315
+ if (!isNodeOfType(initializer, "AssignmentExpression") || initializer.operator !== "??=") return false;
17316
+ const refSymbol = resolveReactRefSymbol(unwrapExpression$3(initializer.left), scopes);
17317
+ if (!refSymbol) return false;
17318
+ return refSymbol.references.every((reference) => {
17319
+ const memberExpression = findTransparentExpressionRoot(reference.identifier).parent;
17320
+ if (!isNodeOfType(memberExpression, "MemberExpression") || unwrapExpression$3(memberExpression.object) !== reference.identifier || getStaticPropertyName(memberExpression) !== "current") return false;
17321
+ const referenceRoot = findTransparentExpressionRoot(memberExpression);
17322
+ const parentNode = referenceRoot.parent;
17323
+ if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.left === referenceRoot) return parentNode === initializer;
17324
+ return !isAssignmentTarget(referenceRoot);
17325
+ });
17326
+ };
17327
+ const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableRefLazyInitialization(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
16914
17328
  //#endregion
16915
17329
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
16916
17330
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -17112,7 +17526,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
17112
17526
  keys.add(depKey);
17113
17527
  continue;
17114
17528
  }
17115
- const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
17529
+ const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
17116
17530
  if (identitySourceKeys) {
17117
17531
  if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
17118
17532
  for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
@@ -17195,6 +17609,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
17195
17609
  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
17610
  return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
17197
17611
  };
17612
+ const isPureDerivedExpression = (expression) => {
17613
+ const candidate = unwrapExpression$3(expression);
17614
+ if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
17615
+ if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
17616
+ if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
17617
+ if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
17618
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
17619
+ if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
17620
+ return false;
17621
+ };
17622
+ const isPureDerivedStatement = (statement) => {
17623
+ if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
17624
+ if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
17625
+ if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
17626
+ return false;
17627
+ };
17628
+ const isPureDerivedFunction = (functionNode) => {
17629
+ if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
17630
+ if (functionNode.async || functionNode.generator) return false;
17631
+ return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
17632
+ };
17633
+ const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
17634
+ if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
17635
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
17636
+ const callExpression = referenceRoot.parent;
17637
+ if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
17638
+ const functionNode = getFunctionValueNode(symbol);
17639
+ if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
17640
+ const sourceKeys = /* @__PURE__ */ new Set();
17641
+ for (const capturedReference of closureCaptures(functionNode, scopes)) {
17642
+ const capturedSymbol = capturedReference.resolvedSymbol;
17643
+ if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
17644
+ if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
17645
+ const capturedKey = computeDepKey(capturedReference);
17646
+ if (!capturedKey) return null;
17647
+ if (capturedKey === capturedSymbol.name) {
17648
+ const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
17649
+ if (nestedSourceKeys) {
17650
+ for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
17651
+ continue;
17652
+ }
17653
+ }
17654
+ sourceKeys.add(capturedKey);
17655
+ }
17656
+ return sourceKeys.size > 0 ? sourceKeys : null;
17657
+ };
17658
+ const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
17659
+ const sourceKeys = /* @__PURE__ */ new Set();
17660
+ for (const expression of expressions) {
17661
+ const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
17662
+ if (!expressionSourceKeys) return null;
17663
+ for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
17664
+ }
17665
+ return sourceKeys;
17666
+ };
17667
+ const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
17668
+ const candidate = unwrapExpression$3(expression);
17669
+ if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
17670
+ if (isNodeOfType(candidate, "Identifier")) {
17671
+ if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
17672
+ const sourceSymbol = scopes.symbolFor(candidate);
17673
+ if (!sourceSymbol) return null;
17674
+ if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
17675
+ 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)) {
17676
+ visitedSymbolIds.add(sourceSymbol.id);
17677
+ const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
17678
+ visitedSymbolIds.delete(sourceSymbol.id);
17679
+ if (sourceKeys) return sourceKeys;
17680
+ }
17681
+ return new Set([sourceSymbol.name]);
17682
+ }
17683
+ if (isNodeOfType(candidate, "MemberExpression")) {
17684
+ if (hasComputedMemberExpression(candidate)) return null;
17685
+ const sourceKey = stringifyMemberChain(candidate);
17686
+ const rootIdentifier = getMemberRootIdentifier(candidate);
17687
+ const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
17688
+ if (!sourceKey || !rootSymbol) return null;
17689
+ if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
17690
+ return new Set([sourceKey]);
17691
+ }
17692
+ if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
17693
+ if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
17694
+ if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
17695
+ candidate.test,
17696
+ candidate.consequent,
17697
+ candidate.alternate
17698
+ ], scopes, visitedSymbolIds);
17699
+ if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
17700
+ if (isNodeOfType(candidate, "NewExpression")) {
17701
+ const callee = unwrapExpression$3(candidate.callee);
17702
+ if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
17703
+ const argumentsToAnalyze = [];
17704
+ for (const argument of candidate.arguments) {
17705
+ if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
17706
+ argumentsToAnalyze.push(argument);
17707
+ }
17708
+ return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
17709
+ }
17710
+ return null;
17711
+ };
17712
+ const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
17713
+ const sourceKeys = /* @__PURE__ */ new Set();
17714
+ let currentNode = assignment;
17715
+ while (currentNode.parent && currentNode.parent !== boundaryFunction) {
17716
+ const parentNode = currentNode.parent;
17717
+ if (isNodeOfType(parentNode, "IfStatement")) {
17718
+ if (parentNode.test === currentNode) return null;
17719
+ const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
17720
+ if (!testSourceKeys) return null;
17721
+ for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
17722
+ } else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
17723
+ currentNode = parentNode;
17724
+ }
17725
+ return currentNode.parent === boundaryFunction ? sourceKeys : null;
17726
+ };
17727
+ const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
17728
+ const referenceRoot = findTransparentExpressionRoot(referenceNode);
17729
+ const callExpression = referenceRoot.parent;
17730
+ return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
17731
+ allowGlobalReactNamespace: true,
17732
+ allowUnboundBareCalls: true,
17733
+ resolveNamedAliases: true
17734
+ });
17735
+ };
17736
+ const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
17737
+ if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
17738
+ const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
17739
+ if (!boundaryFunction) return null;
17740
+ const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
17741
+ if (!capturingFunction || capturingFunction === boundaryFunction) return null;
17742
+ const sourceKeys = /* @__PURE__ */ new Set();
17743
+ if (symbol.initializer) {
17744
+ const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
17745
+ if (!initializerSourceKeys) return null;
17746
+ for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
17747
+ }
17748
+ let writeCount = 0;
17749
+ for (const symbolReference of symbol.references) {
17750
+ if (symbolReference.flag === "read") {
17751
+ if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
17752
+ continue;
17753
+ }
17754
+ if (symbolReference.flag !== "write") return null;
17755
+ const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
17756
+ const assignment = referenceRoot.parent;
17757
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
17758
+ const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
17759
+ const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
17760
+ if (!assignmentSourceKeys || !controlSourceKeys) return null;
17761
+ for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
17762
+ for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
17763
+ writeCount += 1;
17764
+ }
17765
+ return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
17766
+ };
17198
17767
  const isUseCallbackResultDep = (node, scopes) => {
17199
17768
  const rootSymbol = getRootSymbol(node, scopes);
17200
17769
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
@@ -17938,7 +18507,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
17938
18507
  if (!isUsed) continue;
17939
18508
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
17940
18509
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
17941
- if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || !isUnstableInitializer(rootSymbol.initializer)) continue;
18510
+ if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || symbolHasStableValue(rootSymbol, context.scopes) || !isUnstableInitializer(rootSymbol.initializer)) continue;
17942
18511
  context.report({
17943
18512
  node: reportNode,
17944
18513
  message: buildUnstableDepMessage(hookName, declaredKey)
@@ -18254,7 +18823,7 @@ const flattenCalleeName = (callee) => {
18254
18823
  const PRAGMA = "React";
18255
18824
  const isReactFunctionCall = (node, expectedCall) => {
18256
18825
  if (!isNodeOfType(node, "CallExpression")) return false;
18257
- if (getCalleeName$2(node) !== expectedCall) return false;
18826
+ if (getCalleeName$1(node) !== expectedCall) return false;
18258
18827
  if (isNodeOfType(node.callee, "MemberExpression")) {
18259
18828
  const receiver = stripParenExpression(node.callee.object);
18260
18829
  return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
@@ -18563,8 +19132,8 @@ const hooksNoNanInDeps = defineRule({
18563
19132
  severity: "warn",
18564
19133
  recommendation: "Remove `NaN` (or `Number.NaN`) from the dependency array. If a value can be NaN at runtime, normalise it (`Number.isNaN(x) ? 0 : x`) before passing it.",
18565
19134
  create: (context) => ({ CallExpression(node) {
18566
- if (!isHookCall$2(node, HOOKS_WITH_DEP_ARRAY)) return;
18567
- const depsIndex = getCalleeName$2(node) === "useImperativeHandle" ? 2 : 1;
19135
+ if (!isReactHookCall(node, HOOKS_WITH_DEP_ARRAY, context.scopes)) return;
19136
+ const depsIndex = isReactHookCall(node, "useImperativeHandle", context.scopes) ? 2 : 1;
18568
19137
  const depsArgument = node.arguments[depsIndex];
18569
19138
  if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
18570
19139
  for (const element of depsArgument.elements) {
@@ -19735,7 +20304,7 @@ const isImportedSelectAtom = (callExpression) => {
19735
20304
  const isDeferredCallbackPosition$1 = (functionNode) => {
19736
20305
  const parent = functionNode.parent;
19737
20306
  if (isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === functionNode) {
19738
- const hookName = getCalleeName$2(parent);
20307
+ const hookName = getCalleeName$1(parent);
19739
20308
  if (hookName && MEMOIZING_HOOK_NAMES$1.has(hookName)) return true;
19740
20309
  if (hookName && EFFECT_HOOK_NAMES$1.has(hookName) && Boolean(parent.arguments?.[1])) return true;
19741
20310
  }
@@ -29806,7 +30375,7 @@ const DEFERRING_CALLEE_NAMES$1 = new Set([
29806
30375
  "on",
29807
30376
  "once"
29808
30377
  ]);
29809
- const getCalleeName$1 = (callee) => {
30378
+ const getCalleeName = (callee) => {
29810
30379
  if (!callee) return null;
29811
30380
  if (isNodeOfType(callee, "Identifier")) return callee.name;
29812
30381
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
@@ -29818,11 +30387,11 @@ const isDeferredCallbackPosition = (expression) => {
29818
30387
  const parent = parentOf(expression);
29819
30388
  if (!parent) return false;
29820
30389
  if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
29821
- const name = getCalleeName$1(parent.callee);
30390
+ const name = getCalleeName(parent.callee);
29822
30391
  if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
29823
30392
  }
29824
30393
  if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
29825
- const name = getCalleeName$1(parent.callee);
30394
+ const name = getCalleeName(parent.callee);
29826
30395
  if (name && (name.endsWith("Observer") || name === "Promise")) return true;
29827
30396
  }
29828
30397
  if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
@@ -30212,6 +30781,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
30212
30781
  const importDeclaration = declarationNode.parent;
30213
30782
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
30214
30783
  }));
30784
+ const isReactNamespaceReceiver = (analysis, node) => {
30785
+ const receiver = stripParenExpression(node);
30786
+ if (!isNodeOfType(receiver, "Identifier")) return false;
30787
+ const namespaceReference = getRef(analysis, receiver);
30788
+ return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
30789
+ };
30215
30790
  const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
30216
30791
  if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
30217
30792
  const callee = stripParenExpression(declarator.init.callee);
@@ -30220,34 +30795,22 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
30220
30795
  if (!reference?.resolved) return callee.name === hookName;
30221
30796
  return isReactNamedImportReference(reference, hookName);
30222
30797
  }
30223
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
30224
- const namespaceReference = getRef(analysis, callee.object);
30225
- if (!namespaceReference?.resolved) return callee.object.name === "React";
30226
- return isReactNamespaceImportReference(namespaceReference);
30798
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
30799
+ return isReactNamespaceReceiver(analysis, callee.object);
30227
30800
  };
30228
30801
  const isHookCallee$1 = (analysis, node, hookName) => {
30229
30802
  if (!node) return false;
30230
30803
  if (isNodeOfType(node, "Identifier")) {
30231
30804
  if (node.name === hookName) return true;
30232
30805
  if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
30233
- const parent = node.parent;
30234
- if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
30806
+ const receiverRoot = findTransparentExpressionRoot(node);
30807
+ const parent = receiverRoot.parent;
30808
+ if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
30235
30809
  return false;
30236
30810
  }
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
- }
30811
+ if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
30241
30812
  return false;
30242
30813
  };
30243
- const isUseEffect = (node) => {
30244
- if (!node || !isNodeOfType(node, "CallExpression")) return false;
30245
- const callee = node.callee;
30246
- if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
30247
- if (!isNodeOfType(callee, "MemberExpression")) return false;
30248
- const receiver = stripParenExpression(callee.object);
30249
- return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
30250
- };
30251
30814
  const getEffectFn = (analysis, node) => {
30252
30815
  if (!isNodeOfType(node, "CallExpression")) return null;
30253
30816
  const fn = node.arguments?.[0];
@@ -30345,7 +30908,27 @@ const isRefCurrent = (ref) => {
30345
30908
  if (!isNodeOfType(parent.property, "Identifier")) return false;
30346
30909
  return parent.property.name === "current";
30347
30910
  };
30348
- const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isStateSetter(analysis, innerRef));
30911
+ const resolveStateSetterReference = (analysis, ref) => {
30912
+ const visitedReferences = /* @__PURE__ */ new Set();
30913
+ let currentReference = ref;
30914
+ while (currentReference && !visitedReferences.has(currentReference)) {
30915
+ if (isStateSetter(analysis, currentReference)) return currentReference;
30916
+ visitedReferences.add(currentReference);
30917
+ const definitions = currentReference.resolved?.defs ?? [];
30918
+ if (definitions.length !== 1) return null;
30919
+ const definitionNode = definitions[0].node;
30920
+ if (!isNodeOfType(definitionNode, "VariableDeclarator")) return null;
30921
+ if (!isNodeOfType(definitionNode.id, "Identifier")) return null;
30922
+ const declaration = definitionNode.parent;
30923
+ if (!isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
30924
+ if (!definitionNode.init) return null;
30925
+ const initializer = stripParenExpression(definitionNode.init);
30926
+ if (!isNodeOfType(initializer, "Identifier")) return null;
30927
+ currentReference = getRef(analysis, initializer);
30928
+ }
30929
+ return null;
30930
+ };
30931
+ const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => resolveStateSetterReference(analysis, innerRef) !== null);
30349
30932
  const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
30350
30933
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
30351
30934
  const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
@@ -30496,9 +31079,11 @@ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
30496
31079
  };
30497
31080
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
30498
31081
  const getUseStateDecl = (analysis, ref) => {
30499
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
30500
- while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
30501
- return node ?? null;
31082
+ const definition = getUpstreamRefs(analysis, ref).find((upstreamReference) => isState(analysis, upstreamReference) || isStateSetter(analysis, upstreamReference))?.resolved?.defs.find((candidateDefinition) => {
31083
+ const definitionNode = candidateDefinition.node;
31084
+ return isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && isHookCallee$1(analysis, definitionNode.init.callee, "useState");
31085
+ });
31086
+ return definition ? definition.node : null;
30502
31087
  };
30503
31088
  const isCleanupReturnArgument = (analysis, node) => {
30504
31089
  if (isFunctionLike$1(node)) return true;
@@ -30661,7 +31246,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
30661
31246
  if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
30662
31247
  return isSetterWiredToJsxHandler(componentFunction, bindingName);
30663
31248
  };
30664
- const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters = false) => {
31249
+ const isSynchronousFunction = (functionNode) => {
31250
+ const functionMetadata = functionNode;
31251
+ return functionMetadata.async !== true && functionMetadata.generator !== true;
31252
+ };
31253
+ const findBindingVariable = (analysis, bindingIdentifier) => {
31254
+ for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
31255
+ return null;
31256
+ };
31257
+ const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
31258
+ if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
31259
+ const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
31260
+ if (!bindingIdentifier) return null;
31261
+ const variable = findBindingVariable(analysis, bindingIdentifier);
31262
+ if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
31263
+ const definition = variable.defs[0];
31264
+ if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
31265
+ if (definition.type !== "Variable") return null;
31266
+ const declarator = definition.node;
31267
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
31268
+ if (declarator.init === functionNode) return variable;
31269
+ if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
31270
+ return null;
31271
+ };
31272
+ const getJsxEventValueAttribute = (identifier) => {
31273
+ const expression = findTransparentExpressionRoot(identifier);
31274
+ const expressionContainer = expression.parent;
31275
+ if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
31276
+ const attribute = expressionContainer.parent;
31277
+ if (!isNodeOfType(attribute, "JSXAttribute")) return null;
31278
+ const attributeName = getJsxAttributeName(attribute.name);
31279
+ return attributeName && isEventHandlerName(attributeName) ? attribute : null;
31280
+ };
31281
+ const getInlineJsxEventCallbackAttribute = (callExpression) => {
31282
+ const callbackFunction = findEnclosingFunction$1(callExpression);
31283
+ if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
31284
+ return getJsxEventValueAttribute(callbackFunction);
31285
+ };
31286
+ const isReactHookDependencyReference = (identifier) => {
31287
+ const expression = findTransparentExpressionRoot(identifier);
31288
+ const dependencyArray = expression.parent;
31289
+ if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
31290
+ const hookCall = dependencyArray.parent;
31291
+ if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
31292
+ const callee = hookCall.callee;
31293
+ if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
31294
+ return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
31295
+ };
31296
+ const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
31297
+ if (visitedVariables.has(functionVariable)) return false;
31298
+ const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
31299
+ const callExpressions = [];
31300
+ let hasDirectJsxEventReference = false;
31301
+ for (const reference of functionVariable.references) {
31302
+ if (reference.init) continue;
31303
+ const identifier = reference.identifier;
31304
+ if (reference.isWrite()) return false;
31305
+ const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
31306
+ if (jsxEventValueAttribute) {
31307
+ if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
31308
+ continue;
31309
+ }
31310
+ if (isReactHookDependencyReference(identifier)) continue;
31311
+ const callExpression = getCallExpr(reference);
31312
+ if (!callExpression) return false;
31313
+ const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
31314
+ if (jsxEventCallbackAttribute) {
31315
+ if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
31316
+ continue;
31317
+ }
31318
+ callExpressions.push(callExpression);
31319
+ }
31320
+ if (hasDirectJsxEventReference) return true;
31321
+ for (const callExpression of callExpressions) {
31322
+ if (!isNodeReachableWithinFunction(callExpression, context)) continue;
31323
+ const callerFunction = findEnclosingFunction$1(callExpression);
31324
+ if (!callerFunction || callerFunction === componentFunction) continue;
31325
+ const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
31326
+ if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
31327
+ }
31328
+ return false;
31329
+ };
31330
+ const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
30665
31331
  if (!setterRef.resolved) return false;
30666
31332
  const componentFunction = findEnclosingFunction$1(effectNode);
30667
31333
  if (!componentFunction) return false;
@@ -30670,6 +31336,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
30670
31336
  const identifier = reference.identifier;
30671
31337
  if (isAstDescendant(identifier, effectNode)) continue;
30672
31338
  if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
31339
+ if (!isNodeReachableWithinFunction(identifier, context)) continue;
31340
+ const writerFunction = findEnclosingFunction$1(identifier);
31341
+ if (!writerFunction || writerFunction === componentFunction) continue;
31342
+ const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
31343
+ if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
30673
31344
  }
30674
31345
  return false;
30675
31346
  };
@@ -31559,7 +32230,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
31559
32230
  }
31560
32231
  return false;
31561
32232
  };
31562
- const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
32233
+ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
31563
32234
  const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
31564
32235
  if (frames.length === 0) return [];
31565
32236
  const effectHasCleanup = hasCleanup(analysis, effectNode);
@@ -31589,7 +32260,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
31589
32260
  for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
31590
32261
  } else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
31591
32262
  const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
31592
- const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
32263
+ const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
31593
32264
  const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
31594
32265
  if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
31595
32266
  const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
@@ -31601,6 +32272,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
31601
32272
  sourceReferences,
31602
32273
  isDeferred: frame.isDeferred,
31603
32274
  isRenderKnownCopy,
32275
+ isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
31604
32276
  matchesStateInitializer: doesMatchStateInitializer,
31605
32277
  resetsSourceState: false
31606
32278
  });
@@ -31616,22 +32288,71 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
31616
32288
  });
31617
32289
  };
31618
32290
  //#endregion
32291
+ //#region src/plugin/rules/state-and-effects/utils/has-deferred-or-external-effect-work.ts
32292
+ const DEFERRED_MEMBER_NAMES = new Set([
32293
+ "catch",
32294
+ "finally",
32295
+ "then"
32296
+ ]);
32297
+ const hasDeferredOrExternalEffectWork = (analysis, effectNode, scopes) => {
32298
+ const effectFunction = getEffectFn(analysis, effectNode);
32299
+ if (!effectFunction) return false;
32300
+ if (containsFetchCall(effectFunction, { stopAtFunctionBoundary: true })) return true;
32301
+ const effectInvokedFunctions = collectEffectInvokedFunctions(effectFunction);
32302
+ let didFindDeferredOrExternalWork = false;
32303
+ walkAst(effectFunction, (child) => {
32304
+ if (didFindDeferredOrExternalWork) return false;
32305
+ if (child !== effectFunction && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
32306
+ if (isNodeOfType(child, "AssignmentExpression")) {
32307
+ const assignmentTarget = child.left;
32308
+ if ((isNodeOfType(assignmentTarget, "MemberExpression") ? getStaticPropertyName(assignmentTarget) : null)?.startsWith("on") && isFunctionLike$1(child.right)) {
32309
+ didFindDeferredOrExternalWork = true;
32310
+ return false;
32311
+ }
32312
+ }
32313
+ if (!isNodeOfType(child, "CallExpression")) return;
32314
+ if (isSubscribeOrObserveCallExpression(child)) {
32315
+ didFindDeferredOrExternalWork = true;
32316
+ return false;
32317
+ }
32318
+ const localFunction = resolveExactLocalFunction(child.callee, scopes);
32319
+ if (isFunctionLike$1(localFunction) && localFunction.async) {
32320
+ didFindDeferredOrExternalWork = true;
32321
+ return false;
32322
+ }
32323
+ const callee = child.callee;
32324
+ if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) {
32325
+ didFindDeferredOrExternalWork = true;
32326
+ return false;
32327
+ }
32328
+ const memberName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
32329
+ if (memberName && DEFERRED_MEMBER_NAMES.has(memberName)) {
32330
+ didFindDeferredOrExternalWork = true;
32331
+ return false;
32332
+ }
32333
+ });
32334
+ return didFindDeferredOrExternalWork;
32335
+ };
32336
+ //#endregion
31619
32337
  //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
31620
32338
  const noAdjustStateOnPropChange = defineRule({
31621
32339
  id: "no-adjust-state-on-prop-change",
31622
- title: "State synced to a prop inside an effect",
32340
+ title: "State adjusted after a prop changes",
31623
32341
  severity: "warn",
31624
32342
  tags: ["test-noise"],
31625
- 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",
32343
+ recommendation: "Remove the adjustment effect by deriving values during render, resetting the component with a key, or updating related state in the event that changes the prop. Avoid tracking the previous prop in more state, which preserves the duplication. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
31626
32344
  create: (context) => ({ CallExpression(node) {
31627
- if (!isUseEffect(node)) return;
32345
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
31628
32346
  const analysis = getProgramAnalysis(node);
31629
32347
  if (!analysis) return;
31630
32348
  const dependencyReferences = getEffectDepsRefs(analysis, node);
31631
32349
  if (!dependencyReferences) return;
31632
32350
  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)) {
31634
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
32351
+ const facts = collectEffectStateWriteFacts(analysis, context, node, context.filename);
32352
+ if (hasCleanup(analysis, node) || hasDeferredOrExternalEffectWork(analysis, node, context.scopes) || facts.some((fact) => fact.isDeferred)) return;
32353
+ for (const fact of facts) {
32354
+ if (!fact.isSynchronousRenderValue || fact.resetsSourceState) continue;
32355
+ if (fact.sourceReferences.flatMap((reference) => getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) continue;
31635
32356
  context.report({
31636
32357
  node: fact.callExpression,
31637
32358
  message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
@@ -33644,7 +34365,7 @@ const declarationBodyContainsHookCall = (symbol) => {
33644
34365
  walkAst(componentFunction, (descendant) => {
33645
34366
  if (didFindHookCall) return false;
33646
34367
  if (!isNodeOfType(descendant, "CallExpression")) return;
33647
- const calleeName = getCalleeName$2(descendant);
34368
+ const calleeName = getCalleeName$1(descendant);
33648
34369
  if (calleeName && isReactHookName(calleeName)) {
33649
34370
  didFindHookCall = true;
33650
34371
  return false;
@@ -33669,7 +34390,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
33669
34390
  if (isNodeOfType(parent, "ArrowFunctionExpression")) {
33670
34391
  if (parent.body !== current) return false;
33671
34392
  const grandparent = parent.parent;
33672
- return isNodeOfType(grandparent, "CallExpression") && getCalleeName$2(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
34393
+ return isNodeOfType(grandparent, "CallExpression") && getCalleeName$1(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
33673
34394
  }
33674
34395
  if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
33675
34396
  current = parent;
@@ -34351,7 +35072,7 @@ const noChainStateUpdates = defineRule({
34351
35072
  if (!callExpr) continue;
34352
35073
  if (!isReachableFromStateTrigger(callExpr)) continue;
34353
35074
  if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
34354
- const declarator = getUseStateDeclarator(ref);
35075
+ const declarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
34355
35076
  if (declarator) domSyncedStateDeclarators.add(declarator);
34356
35077
  }
34357
35078
  for (const ref of effectFnRefs) {
@@ -34360,7 +35081,7 @@ const noChainStateUpdates = defineRule({
34360
35081
  if (!callExpr) continue;
34361
35082
  if (!isReachableFromStateTrigger(callExpr)) continue;
34362
35083
  if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
34363
- const setterDeclarator = getUseStateDeclarator(ref);
35084
+ const setterDeclarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
34364
35085
  if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
34365
35086
  const isSelfTargeting = setterDeclarator !== null && stateDepDeclarators.has(setterDeclarator);
34366
35087
  const setterArguments = isNodeOfType(callExpr, "CallExpression") ? callExpr.arguments ?? [] : [];
@@ -36201,10 +36922,10 @@ const noDerivedState = defineRule({
36201
36922
  for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
36202
36923
  } }).visitors,
36203
36924
  CallExpression(node) {
36204
- if (!isUseEffect(node)) return;
36925
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
36205
36926
  const analysis = getProgramAnalysis(node);
36206
36927
  if (!analysis) return;
36207
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
36928
+ for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
36208
36929
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
36209
36930
  reportStateWrite(fact.callExpression, fact.stateDeclarator);
36210
36931
  }
@@ -36221,10 +36942,10 @@ const noDerivedStateEffect = defineRule({
36221
36942
  tags: ["test-noise"],
36222
36943
  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
36944
  create: (context) => ({ CallExpression(node) {
36224
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
36945
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
36225
36946
  const analysis = getProgramAnalysis(node);
36226
36947
  if (!analysis) return;
36227
- if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
36948
+ if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
36228
36949
  context.report({
36229
36950
  node,
36230
36951
  message: "You pay an extra render for state you can derive from other values."
@@ -36344,7 +37065,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
36344
37065
  if (isFunctionLike$1(cursor)) {
36345
37066
  const parent = cursor.parent ?? null;
36346
37067
  if (parent && isNodeOfType(parent, "CallExpression")) {
36347
- const calleeName = getCalleeName$2(parent);
37068
+ const calleeName = getCalleeName$1(parent);
36348
37069
  if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
36349
37070
  }
36350
37071
  }
@@ -36415,7 +37136,7 @@ const isNonHandlerHookCallback = (functionNode) => {
36415
37136
  const parent = functionNode.parent ?? null;
36416
37137
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
36417
37138
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
36418
- const calleeName = getCalleeName$2(parent);
37139
+ const calleeName = getCalleeName$1(parent);
36419
37140
  return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
36420
37141
  };
36421
37142
  const isHandlerShapedReseed = (setterCall, componentFunction) => {
@@ -36497,7 +37218,7 @@ const noDerivedUseState = defineRule({
36497
37218
  return {
36498
37219
  ...propStackTracker.visitors,
36499
37220
  CallExpression(node) {
36500
- if (!isHookCall$2(node, "useState") || !node.arguments?.length) return;
37221
+ if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
36501
37222
  const seed = unwrapInitializerSeed(node.arguments[0]);
36502
37223
  const reportStalePropCopy = (propName) => {
36503
37224
  if (isIntentionalSnapshotState(node)) return;
@@ -37173,7 +37894,7 @@ const noDirectMutationState = defineRule({
37173
37894
  const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
37174
37895
  //#endregion
37175
37896
  //#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
37176
- const collectUseStateBindings = (componentBody) => {
37897
+ const collectUseStateBindings = (componentBody, scopes) => {
37177
37898
  const bindings = [];
37178
37899
  if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
37179
37900
  for (const statement of componentBody.body ?? []) {
@@ -37186,7 +37907,7 @@ const collectUseStateBindings = (componentBody) => {
37186
37907
  const setterElement = elements[1];
37187
37908
  if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
37188
37909
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
37189
- if (!isHookCall$2(declarator.init, "useState")) continue;
37910
+ if (!isReactHookCall(declarator.init, "useState", scopes)) continue;
37190
37911
  bindings.push({
37191
37912
  valueName: valueElement.name,
37192
37913
  setterName: setterElement.name,
@@ -37353,7 +38074,7 @@ const noDirectStateMutation = defineRule({
37353
38074
  create: (context) => {
37354
38075
  const checkComponent = (componentBody) => {
37355
38076
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
37356
- const bindings = collectUseStateBindings(componentBody);
38077
+ const bindings = collectUseStateBindings(componentBody, context.scopes);
37357
38078
  if (bindings.length === 0) return;
37358
38079
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
37359
38080
  const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
@@ -37913,25 +38634,31 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
37913
38634
  };
37914
38635
  //#endregion
37915
38636
  //#region src/plugin/rules/state-and-effects/no-effect-chain.ts
37916
- const findTopLevelEffectCalls = (componentBody) => {
38637
+ const findTopLevelEffectCalls = (componentBody, scopes) => {
37917
38638
  const effectCalls = [];
37918
38639
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
37919
38640
  for (const statement of componentBody.body ?? []) {
37920
38641
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
37921
38642
  const expression = unwrapDiscardedExpression(statement);
37922
38643
  if (!isNodeOfType(expression, "CallExpression")) continue;
37923
- if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
38644
+ if (!isReactHookCall(expression, EFFECT_HOOK_NAMES$1, scopes)) continue;
37924
38645
  effectCalls.push(expression);
37925
38646
  }
37926
38647
  return effectCalls;
37927
38648
  };
37928
- const collectDepIdentifierNames = (effectNode) => {
37929
- const depNames = /* @__PURE__ */ new Set();
37930
- if (!isNodeOfType(effectNode, "CallExpression")) return depNames;
38649
+ const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) => {
38650
+ const dependencyStateSymbolIds = /* @__PURE__ */ new Set();
38651
+ if (!isNodeOfType(effectNode, "CallExpression")) return dependencyStateSymbolIds;
37931
38652
  const depsNode = effectNode.arguments?.[1];
37932
- if (!isNodeOfType(depsNode, "ArrayExpression")) return depNames;
37933
- for (const element of depsNode.elements ?? []) if (isNodeOfType(element, "Identifier")) depNames.add(element.name);
37934
- return depNames;
38653
+ if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyStateSymbolIds;
38654
+ for (const element of depsNode.elements ?? []) {
38655
+ if (!element || isNodeOfType(element, "SpreadElement")) continue;
38656
+ const rootIdentifier = getRootIdentifier$1(element);
38657
+ if (!isNodeOfType(rootIdentifier, "Identifier")) continue;
38658
+ const symbol = resolveConstIdentifierAlias(rootIdentifier, scopes, true);
38659
+ if (symbol && stateSymbolIds.has(symbol.id)) dependencyStateSymbolIds.add(symbol.id);
38660
+ }
38661
+ return dependencyStateSymbolIds;
37935
38662
  };
37936
38663
  const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
37937
38664
  const analysisFunctions = new Set([effectCallback]);
@@ -38037,12 +38764,13 @@ const readStaticSetterValue = (setterCall, scopes) => {
38037
38764
  if (updater) return readStaticUpdaterReturnValue(updater, scopes);
38038
38765
  return readStaticEffectValue(argument, scopes, null, null);
38039
38766
  };
38040
- const collectStateWritesInEffect = (analysisFunctions, setterToStateName, scopes) => {
38767
+ const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName, scopes) => {
38041
38768
  const stateWrites = /* @__PURE__ */ new Map();
38042
38769
  visitSynchronousFunctionBodies(analysisFunctions, (child) => {
38043
38770
  if (!isNodeOfType(child, "CallExpression")) return;
38044
38771
  if (!isNodeOfType(child.callee, "Identifier")) return;
38045
- const stateName = setterToStateName.get(child.callee.name);
38772
+ const setterSymbol = resolveConstIdentifierAlias(child.callee, scopes, true);
38773
+ const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
38046
38774
  if (!stateName) return;
38047
38775
  const writeInfo = stateWrites.get(stateName) ?? {
38048
38776
  values: /* @__PURE__ */ new Set(),
@@ -38128,11 +38856,12 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
38128
38856
  "keys",
38129
38857
  "values"
38130
38858
  ]);
38131
- const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
38859
+ const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
38132
38860
  if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
38133
38861
  if (isNodeOfType(returnedValue, "CallExpression")) {
38134
38862
  if (isNodeOfType(returnedValue.callee, "Identifier")) {
38135
- if (setterToStateName.has(returnedValue.callee.name)) return false;
38863
+ const setterSymbol = resolveConstIdentifierAlias(returnedValue.callee, scopes, true);
38864
+ if (setterToStateName.has(returnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
38136
38865
  if (isSetterIdentifier(returnedValue.callee.name)) return true;
38137
38866
  }
38138
38867
  return isCleanupReturn(returnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
@@ -38156,7 +38885,7 @@ const collectStorageHookSetterNames = (componentBody) => {
38156
38885
  for (const declarator of statement.declarations ?? []) {
38157
38886
  if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
38158
38887
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
38159
- const calleeName = getCalleeName$2(declarator.init);
38888
+ const calleeName = getCalleeName$1(declarator.init);
38160
38889
  if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
38161
38890
  for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
38162
38891
  }
@@ -38299,11 +39028,11 @@ const isExternalSyncNode = (node) => {
38299
39028
  const receiverRootName = getRootIdentifierName(node.callee.object);
38300
39029
  return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
38301
39030
  };
38302
- const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
39031
+ const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
38303
39032
  if (!isFunctionLike$1(effectCallback)) return false;
38304
39033
  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;
39034
+ if (isFunctionShapedReturn(effectCallback.body, setterToStateName, setterSymbolIdToStateName, scopes, false)) return true;
39035
+ } else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, setterSymbolIdToStateName, scopes, true)) return true;
38307
39036
  let didFindExternalCall = false;
38308
39037
  visitSynchronousFunctionBodies(analysisFunctions, (child) => {
38309
39038
  if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
@@ -38319,10 +39048,11 @@ const noEffectChain = defineRule({
38319
39048
  create: (context) => {
38320
39049
  const checkComponent = (componentBody) => {
38321
39050
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38322
- const useStateBindings = collectUseStateBindings(componentBody);
39051
+ const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
38323
39052
  if (useStateBindings.length === 0) return;
38324
39053
  const setterToStateName = /* @__PURE__ */ new Map();
38325
39054
  const stateSymbolIds = /* @__PURE__ */ new Map();
39055
+ const setterSymbolIdToStateName = /* @__PURE__ */ new Map();
38326
39056
  for (const binding of useStateBindings) {
38327
39057
  setterToStateName.set(binding.setterName, binding.valueName);
38328
39058
  if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
@@ -38331,21 +39061,27 @@ const noEffectChain = defineRule({
38331
39061
  const stateSymbol = context.scopes.symbolFor(stateIdentifier);
38332
39062
  if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
38333
39063
  }
39064
+ const setterIdentifier = binding.declarator.id.elements[1];
39065
+ if (isNodeOfType(setterIdentifier, "Identifier")) {
39066
+ const setterSymbol = context.scopes.symbolFor(setterIdentifier);
39067
+ if (setterSymbol) setterSymbolIdToStateName.set(setterSymbol.id, binding.valueName);
39068
+ }
38334
39069
  }
38335
39070
  const storageSetterNames = collectStorageHookSetterNames(componentBody);
39071
+ const stateSymbolIdSet = new Set(stateSymbolIds.values());
38336
39072
  const effectInfos = [];
38337
- for (const effectCall of findTopLevelEffectCalls(componentBody)) {
39073
+ for (const effectCall of findTopLevelEffectCalls(componentBody, context.scopes)) {
38338
39074
  const callback = getEffectCallback(effectCall, context.scopes);
38339
39075
  if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
38340
39076
  const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
38341
- const stateWrites = collectStateWritesInEffect(analysisFunctions, setterToStateName, context.scopes);
39077
+ const stateWrites = collectStateWritesInEffect(analysisFunctions, setterSymbolIdToStateName, context.scopes);
38342
39078
  const writtenStateNames = new Set(stateWrites.keys());
38343
39079
  effectInfos.push({
38344
39080
  node: effectCall,
38345
- depNames: collectDepIdentifierNames(effectCall),
39081
+ dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
38346
39082
  stateWrites,
38347
39083
  analysisFunctions,
38348
- isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
39084
+ isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
38349
39085
  });
38350
39086
  }
38351
39087
  if (effectInfos.length < 2) return;
@@ -38356,10 +39092,11 @@ const noEffectChain = defineRule({
38356
39092
  for (const readerEffect of effectInfos) {
38357
39093
  if (readerEffect === writerEffect) continue;
38358
39094
  if (readerEffect.isExternalSync) continue;
38359
- if (readerEffect.depNames.size === 0) continue;
39095
+ if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
38360
39096
  let chainedStateName = null;
38361
39097
  for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
38362
- if (!readerEffect.depNames.has(writtenName)) continue;
39098
+ const writtenStateSymbolId = stateSymbolIds.get(writtenName);
39099
+ if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
38363
39100
  if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
38364
39101
  chainedStateName = writtenName;
38365
39102
  break;
@@ -38636,7 +39373,7 @@ const noEffectEventHandler = defineRule({
38636
39373
  return {
38637
39374
  ...propStackTracker.visitors,
38638
39375
  CallExpression(node) {
38639
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
39376
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
38640
39377
  const callback = getEffectCallback(node);
38641
39378
  if (!callback) return;
38642
39379
  const analysis = getProgramAnalysis(node);
@@ -38756,14 +39493,14 @@ const noEffectEventInDeps = defineRule({
38756
39493
  if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
38757
39494
  const initializer = declaratorNode.init;
38758
39495
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
38759
- if (!isHookCall$2(initializer, "useEffectEvent")) return;
39496
+ if (!isReactHookCall(initializer, "useEffectEvent", context.scopes)) return;
38760
39497
  if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
38761
39498
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
38762
39499
  } });
38763
39500
  return {
38764
39501
  ...componentBindings.visitors,
38765
39502
  CallExpression(node) {
38766
- if (!isHookCall$2(node, HOOKS_WITH_DEPS) || node.arguments.length < 2) return;
39503
+ if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
38767
39504
  if (!componentBindings.isInsideComponent()) return;
38768
39505
  const depsNode = node.arguments[1];
38769
39506
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
@@ -38820,7 +39557,7 @@ const noEffectWithFreshDeps = defineRule({
38820
39557
  node: finding.reportNode,
38821
39558
  message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
38822
39559
  });
38823
- if (!isHookCall$2(node, HOOKS_WITH_DEPS)) return;
39560
+ if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes)) return;
38824
39561
  const args = node.arguments ?? [];
38825
39562
  if (args.length < 2) return;
38826
39563
  const depsNode = args[1];
@@ -39081,7 +39818,7 @@ const noEventHandler = defineRule({
39081
39818
  severity: "warn",
39082
39819
  recommendation: "Run the side effect in the event handler that triggers it, instead of watching its state from a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers",
39083
39820
  create: (context) => ({ CallExpression(node) {
39084
- if (!isUseEffect(node)) return;
39821
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
39085
39822
  const analysis = getProgramAnalysis(node);
39086
39823
  if (!analysis || hasCleanup(analysis, node)) return;
39087
39824
  const frames = collectBoundedEffectExecutionFrames(analysis, node);
@@ -39543,25 +40280,25 @@ const addDeclarationBindings = (statement, scope) => {
39543
40280
  }
39544
40281
  if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
39545
40282
  };
39546
- const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
40283
+ const collectRenderReachableNamesFromStatements = (statements, names, scope, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
39547
40284
  let hasReturn = false;
39548
- for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, eventHandlerReferenceNames)) hasReturn = true;
40285
+ for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, scopes, eventHandlerReferenceNames)) hasReturn = true;
39549
40286
  else addDeclarationBindings(statement, scope);
39550
40287
  return hasReturn;
39551
40288
  };
39552
- const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
40289
+ const collectRenderReachableNamesFromStatement = (statement, names, scope, scopes, eventHandlerReferenceNames) => {
39553
40290
  if (isNodeOfType(statement, "ReturnStatement")) {
39554
40291
  if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
39555
40292
  return true;
39556
40293
  }
39557
- if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) && !EFFECT_HOOK_NAMES$1.has(statement.expression.callee.name)) {
40294
+ if (isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "CallExpression") && (isNodeOfType(statement.expression.callee, "Identifier") && isReactHookName(statement.expression.callee.name) || isReactHookCall(statement.expression, BUILTIN_HOOK_NAMES, scopes)) && !isReactHookCall(statement.expression, EFFECT_HOOK_NAMES$1, scopes)) {
39558
40295
  for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
39559
40296
  return false;
39560
40297
  }
39561
- if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), eventHandlerReferenceNames);
40298
+ if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), scopes, eventHandlerReferenceNames);
39562
40299
  if (isNodeOfType(statement, "IfStatement")) {
39563
- const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
39564
- const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, eventHandlerReferenceNames) : false;
40300
+ const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, scopes, eventHandlerReferenceNames);
40301
+ const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, scopes, eventHandlerReferenceNames) : false;
39565
40302
  if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
39566
40303
  return consequentHasReturn || alternateHasReturn;
39567
40304
  }
@@ -39569,7 +40306,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
39569
40306
  let hasReturn = false;
39570
40307
  for (const switchCase of statement.cases ?? []) {
39571
40308
  const caseScope = createBlockBindingScope(scope);
39572
- if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
40309
+ if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, scopes, eventHandlerReferenceNames)) continue;
39573
40310
  hasReturn = true;
39574
40311
  if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
39575
40312
  }
@@ -39577,25 +40314,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
39577
40314
  return hasReturn;
39578
40315
  }
39579
40316
  if (isNodeOfType(statement, "TryStatement")) {
39580
- const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
39581
- const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
39582
- const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, eventHandlerReferenceNames) : false;
40317
+ const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, scopes, eventHandlerReferenceNames);
40318
+ const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, scopes, eventHandlerReferenceNames) : false;
40319
+ const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, scopes, eventHandlerReferenceNames) : false;
39583
40320
  return blockHasReturn || handlerHasReturn || finalizerHasReturn;
39584
40321
  }
39585
40322
  if (isNodeOfType(statement, "CatchClause")) {
39586
40323
  const catchScope = createBlockBindingScope(scope);
39587
40324
  addPatternBindings(statement.param, catchScope);
39588
- return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
40325
+ return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, scopes, eventHandlerReferenceNames);
39589
40326
  }
39590
40327
  if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
39591
- const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
40328
+ const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
39592
40329
  if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
39593
40330
  return bodyHasReturn;
39594
40331
  }
39595
40332
  if (isNodeOfType(statement, "ForStatement")) {
39596
40333
  const loopScope = createBlockBindingScope(scope);
39597
40334
  if (statement.init) addDeclarationBindings(statement.init, loopScope);
39598
- if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
40335
+ if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
39599
40336
  if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
39600
40337
  if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
39601
40338
  if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
@@ -39605,22 +40342,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
39605
40342
  const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
39606
40343
  const loopScope = createBlockBindingScope(scope);
39607
40344
  if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
39608
- if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
40345
+ if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
39609
40346
  addNames(names, rightNames);
39610
40347
  return true;
39611
40348
  }
39612
- if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
40349
+ if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
39613
40350
  if (isNodeOfType(statement, "WithStatement")) {
39614
- const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
40351
+ const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
39615
40352
  if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
39616
40353
  return bodyHasReturn;
39617
40354
  }
39618
40355
  return false;
39619
40356
  };
39620
- const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
40357
+ const collectRenderReachableNames = (componentBody, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
39621
40358
  const names = /* @__PURE__ */ new Set();
39622
40359
  if (!isNodeOfType(componentBody, "BlockStatement")) return names;
39623
- collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
40360
+ collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), scopes, eventHandlerReferenceNames);
39624
40361
  return names;
39625
40362
  };
39626
40363
  //#endregion
@@ -39643,42 +40380,36 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
39643
40380
  };
39644
40381
  //#endregion
39645
40382
  //#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
39646
- const isUseCallbackCall = (node) => isNodeOfType(node, "CallExpression") && getCalleeName(node.callee) === "useCallback";
39647
- const getCalleeName = (node) => {
39648
- if (isNodeOfType(node, "Identifier")) return node.name;
39649
- if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
39650
- return null;
39651
- };
39652
- const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
39653
- if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
40383
+ const isFunctionLikeReference = (node, functionLikeLocalNames, scope, scopes) => {
40384
+ if (isInlineFunctionExpression(node) || isReactHookCall(node, "useCallback", scopes)) return true;
39654
40385
  if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
39655
40386
  const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
39656
40387
  return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
39657
40388
  };
39658
- const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
40389
+ const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope, scopes) => {
39659
40390
  if (!isNodeOfType(node, "ObjectExpression")) return;
39660
40391
  for (const property of node.properties ?? []) {
39661
40392
  if (!isNodeOfType(property, "Property")) continue;
39662
40393
  const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
39663
40394
  if (!propertyName) continue;
39664
- if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
40395
+ if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope, scopes)) continue;
39665
40396
  functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
39666
40397
  }
39667
40398
  };
39668
- const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
40399
+ const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
39669
40400
  if (!isNodeOfType(statement, "VariableDeclaration")) return;
39670
40401
  const declarationScope = getVariableDeclarationScope(statement, scope);
39671
40402
  for (const declarator of statement.declarations ?? []) {
39672
40403
  const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
39673
40404
  if (!declarator.init) continue;
39674
- const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
40405
+ const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope, scopes);
39675
40406
  for (const declaredBindingName of declaredBindingNames) {
39676
40407
  if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
39677
- addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
40408
+ addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope, scopes);
39678
40409
  }
39679
40410
  }
39680
40411
  };
39681
- const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
40412
+ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
39682
40413
  if (isNodeOfType(statement, "FunctionDeclaration")) {
39683
40414
  if (statement.id) {
39684
40415
  const declaredBindingNames = addPatternBindings(statement.id, scope);
@@ -39687,61 +40418,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope)
39687
40418
  return;
39688
40419
  }
39689
40420
  if (isNodeOfType(statement, "VariableDeclaration")) {
39690
- addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
40421
+ addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope, scopes);
39691
40422
  return;
39692
40423
  }
39693
40424
  if (isNodeOfType(statement, "BlockStatement")) {
39694
- collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
40425
+ collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
39695
40426
  return;
39696
40427
  }
39697
40428
  if (isNodeOfType(statement, "IfStatement")) {
39698
- collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
39699
- if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope);
40429
+ collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope, scopes);
40430
+ if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope, scopes);
39700
40431
  return;
39701
40432
  }
39702
40433
  if (isNodeOfType(statement, "SwitchStatement")) {
39703
- for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope));
40434
+ for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
39704
40435
  return;
39705
40436
  }
39706
40437
  if (isNodeOfType(statement, "TryStatement")) {
39707
- collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
40438
+ collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope, scopes);
39708
40439
  if (statement.handler) {
39709
40440
  const catchScope = createBlockBindingScope(scope);
39710
40441
  addPatternBindings(statement.handler.param, catchScope);
39711
- collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
40442
+ collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope, scopes);
39712
40443
  }
39713
- if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
40444
+ if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope, scopes);
39714
40445
  return;
39715
40446
  }
39716
40447
  if (isNodeOfType(statement, "ForStatement")) {
39717
40448
  const loopScope = createBlockBindingScope(scope);
39718
- if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
39719
- collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
40449
+ if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope, scopes);
40450
+ collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
39720
40451
  return;
39721
40452
  }
39722
40453
  if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
39723
40454
  const loopScope = createBlockBindingScope(scope);
39724
- if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
40455
+ if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope, scopes);
39725
40456
  else addPatternBindings(statement.left, loopScope);
39726
- collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
40457
+ collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
39727
40458
  return;
39728
40459
  }
39729
40460
  if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
39730
- collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
40461
+ collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
39731
40462
  return;
39732
40463
  }
39733
- if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
40464
+ if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
39734
40465
  };
39735
- const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
39736
- for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope);
40466
+ const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope, scopes) => {
40467
+ for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope, scopes);
39737
40468
  };
39738
- const collectFunctionLikeLocalNames = (componentBody) => {
40469
+ const collectFunctionLikeLocalNames = (componentBody, scopes) => {
39739
40470
  const functionLikeLocalNames = /* @__PURE__ */ new Set();
39740
40471
  if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
39741
40472
  let previousSize = -1;
39742
40473
  while (previousSize !== functionLikeLocalNames.size) {
39743
40474
  previousSize = functionLikeLocalNames.size;
39744
- collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
40475
+ collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope(), scopes);
39745
40476
  }
39746
40477
  return functionLikeLocalNames;
39747
40478
  };
@@ -39781,17 +40512,17 @@ const noEventTriggerState = defineRule({
39781
40512
  create: (context) => {
39782
40513
  const checkComponent = (componentBody) => {
39783
40514
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
39784
- const useStateBindings = collectUseStateBindings(componentBody);
40515
+ const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
39785
40516
  if (useStateBindings.length === 0) return;
39786
40517
  const analysis = getProgramAnalysis(componentBody);
39787
40518
  if (!analysis) return;
39788
40519
  const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
39789
- const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
40520
+ const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
39790
40521
  const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
39791
- const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
40522
+ const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames), dependencyGraph);
39792
40523
  walkAst(componentBody, (effectCall) => {
39793
40524
  if (!isNodeOfType(effectCall, "CallExpression")) return;
39794
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) return;
40525
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) return;
39795
40526
  if ((effectCall.arguments?.length ?? 0) < 2) return;
39796
40527
  const depsNode = effectCall.arguments[1];
39797
40528
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
@@ -40175,6 +40906,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
40175
40906
  "scrollHeight"
40176
40907
  ]);
40177
40908
  const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
40909
+ const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
40910
+ "blur",
40911
+ "focus",
40912
+ "restoreSelection",
40913
+ "scroll",
40914
+ "scrollBy",
40915
+ "scrollIntoView",
40916
+ "scrollTo",
40917
+ "setRangeText",
40918
+ "setSelectionRange"
40919
+ ]);
40178
40920
  const subtreeReadsDomMeasurement = (root) => {
40179
40921
  if (!root) return false;
40180
40922
  let found = false;
@@ -40193,29 +40935,66 @@ const subtreeReadsDomMeasurement = (root) => {
40193
40935
  });
40194
40936
  return found;
40195
40937
  };
40196
- const collectMeasuringFunctionNames = (program) => {
40938
+ const collectFunctionNamesMatchingBody = (program, matchesBody) => {
40197
40939
  const names = /* @__PURE__ */ new Set();
40198
40940
  walkAst(program, (child) => {
40199
40941
  if (isNodeOfType(child, "FunctionDeclaration")) {
40200
- if (child.id && isNodeOfType(child.id, "Identifier") && subtreeReadsDomMeasurement(child.body)) names.add(child.id.name);
40942
+ if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
40201
40943
  return;
40202
40944
  }
40203
40945
  if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
40204
40946
  let functionValue = child.init;
40205
40947
  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) && subtreeReadsDomMeasurement(functionValue.body)) names.add(child.id.name);
40948
+ if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
40207
40949
  });
40208
40950
  return names;
40209
40951
  };
40210
- const callsAnyName = (root, names) => {
40211
- if (!root || names.size === 0) return false;
40952
+ const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
40953
+ const subtreeMutatesDomImperatively = (root) => {
40954
+ if (!root || isFunctionLike$1(root)) return false;
40212
40955
  let found = false;
40213
40956
  walkAst(root, (child) => {
40214
40957
  if (found) return false;
40958
+ if (child !== root && isFunctionLike$1(child)) return false;
40959
+ if (!isNodeOfType(child, "CallExpression")) return;
40960
+ const callee = stripParenExpression(child.callee);
40961
+ const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
40962
+ if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
40963
+ found = true;
40964
+ return false;
40965
+ }
40966
+ });
40967
+ return found;
40968
+ };
40969
+ const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
40970
+ const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
40971
+ if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
40972
+ let found = false;
40973
+ walkAst(root, (child) => {
40974
+ if (found) return false;
40975
+ if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
40215
40976
  if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
40216
40977
  });
40217
40978
  return found;
40218
40979
  };
40980
+ const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
40981
+ let statement = call;
40982
+ let parent = statement.parent;
40983
+ while (parent) {
40984
+ const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
40985
+ if (statements) {
40986
+ const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
40987
+ if (statementIndex >= 0) {
40988
+ const nextStatement = statements[statementIndex + 1];
40989
+ return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
40990
+ }
40991
+ }
40992
+ if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
40993
+ statement = parent;
40994
+ parent = parent.parent;
40995
+ }
40996
+ return false;
40997
+ };
40219
40998
  const isInsideStartViewTransition = (node) => {
40220
40999
  let cursor = node.parent;
40221
41000
  while (cursor) {
@@ -40256,11 +41035,12 @@ const importsImperativeDomLibrary = (program) => {
40256
41035
  };
40257
41036
  const hasExemptFlushSyncCall = (program, localName) => {
40258
41037
  const measuringFunctionNames = collectMeasuringFunctionNames(program);
41038
+ const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
40259
41039
  let exempt = false;
40260
41040
  walkAst(program, (child) => {
40261
41041
  if (exempt) return false;
40262
41042
  if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
40263
- if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
41043
+ if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
40264
41044
  exempt = true;
40265
41045
  return false;
40266
41046
  }
@@ -40825,41 +41605,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
40825
41605
  if (leftResult === false && rightResult === false) return false;
40826
41606
  return null;
40827
41607
  };
40828
- const readHydrationConditionResult = (expression, context, runtime) => {
41608
+ const readHydrationConditionResult = (expression, context, runtime, state) => {
40829
41609
  const unwrappedExpression = stripParenExpression(expression);
40830
41610
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
40831
41611
  if (predicateMatch) return predicateMatch[`${runtime}Result`];
40832
41612
  const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
40833
41613
  if (staticResult !== null) return staticResult;
41614
+ const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
41615
+ const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
41616
+ if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41617
+ state.visitedSymbolIds.add(expressionSymbol.id);
41618
+ const result = readHydrationConditionResult(parameterValue, context, runtime, state);
41619
+ state.visitedSymbolIds.delete(expressionSymbol.id);
41620
+ return result;
41621
+ }
41622
+ if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
41623
+ state.visitedSymbolIds.add(expressionSymbol.id);
41624
+ const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
41625
+ state.visitedSymbolIds.delete(expressionSymbol.id);
41626
+ return result;
41627
+ }
41628
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41629
+ const callArguments = unwrappedExpression.arguments ?? [];
41630
+ if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41631
+ allowGlobalReactNamespace: true,
41632
+ resolveNamedAliases: true
41633
+ })) {
41634
+ const callbackArgument = callArguments[0];
41635
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41636
+ const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41637
+ return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
41638
+ }
41639
+ const callee = stripParenExpression(unwrappedExpression.callee);
41640
+ 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);
41641
+ const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41642
+ 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;
41643
+ const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41644
+ for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41645
+ const parameter = helperFunction.params[parameterIndex];
41646
+ const argument = callArguments[parameterIndex];
41647
+ if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41648
+ const parameterSymbol = context.scopes.symbolFor(parameter);
41649
+ if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41650
+ }
41651
+ return readHydrationFunctionResult(helperFunction, context, runtime, {
41652
+ ...state,
41653
+ parameterValuesBySymbolId
41654
+ });
41655
+ }
40834
41656
  if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
40835
- const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
41657
+ const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
40836
41658
  return argumentResult === null ? null : !argumentResult;
40837
41659
  }
40838
41660
  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));
41661
+ return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
41662
+ };
41663
+ const readHydrationStatementResult = (statement, context, runtime, state) => {
41664
+ if (isNodeOfType(statement, "ReturnStatement")) return {
41665
+ didReturn: true,
41666
+ value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
41667
+ };
41668
+ if (isNodeOfType(statement, "BlockStatement")) {
41669
+ for (const childStatement of statement.body) {
41670
+ const result = readHydrationStatementResult(childStatement, context, runtime, state);
41671
+ if (result.didReturn) return result;
41672
+ if (statementAlwaysExits(childStatement)) break;
41673
+ }
41674
+ return {
41675
+ didReturn: false,
41676
+ value: null
41677
+ };
41678
+ }
41679
+ if (!isNodeOfType(statement, "IfStatement")) return {
41680
+ didReturn: false,
41681
+ value: null
41682
+ };
41683
+ const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
41684
+ if (conditionResult !== null) {
41685
+ const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
41686
+ return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
41687
+ didReturn: false,
41688
+ value: null
41689
+ };
41690
+ }
41691
+ const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
41692
+ const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
41693
+ didReturn: false,
41694
+ value: null
41695
+ };
41696
+ return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
41697
+ didReturn: consequentResult.didReturn || alternateResult.didReturn,
41698
+ value: null
41699
+ };
41700
+ };
41701
+ const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
41702
+ if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41703
+ state.visitedFunctionNodes.add(functionNode);
41704
+ const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
41705
+ state.visitedFunctionNodes.delete(functionNode);
41706
+ return result;
41707
+ };
41708
+ const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
41709
+ const left = stripParenExpression(leftExpression);
41710
+ const right = stripParenExpression(rightExpression);
41711
+ if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
41712
+ const leftSymbol = scopes.symbolFor(left);
41713
+ const rightSymbol = scopes.symbolFor(right);
41714
+ return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
41715
+ }
41716
+ if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
41717
+ if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
41718
+ const rightArguments = right.arguments ?? [];
41719
+ return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
41720
+ const rightArgument = rightArguments[index];
41721
+ return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
41722
+ });
41723
+ }
41724
+ return true;
40840
41725
  };
40841
- const matchHydrationCondition = (expression, context) => {
41726
+ const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
41727
+ if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
41728
+ const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
41729
+ const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
41730
+ return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
41731
+ };
41732
+ const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
41733
+ const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
41734
+ return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
41735
+ };
41736
+ const matchHydrationConditionInternal = (expression, context, state) => {
40842
41737
  const unwrappedExpression = stripParenExpression(expression);
40843
41738
  const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
40844
41739
  if (predicateMatch) return {
40845
41740
  predicateMatch,
40846
41741
  predicateNode: unwrappedExpression
40847
41742
  };
40848
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
40849
- if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
40850
- const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
40851
- const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
40852
- if (leftMatch && rightMatch) {
40853
- const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
40854
- const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
40855
- return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
41743
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
41744
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
41745
+ const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
41746
+ if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
41747
+ state.visitedSymbolIds.add(symbol.id);
41748
+ const match = matchHydrationConditionInternal(parameterValue, context, state);
41749
+ state.visitedSymbolIds.delete(symbol.id);
41750
+ return match;
41751
+ }
41752
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
41753
+ state.visitedSymbolIds.add(symbol.id);
41754
+ const match = matchHydrationConditionInternal(symbol.initializer, context, state);
41755
+ state.visitedSymbolIds.delete(symbol.id);
41756
+ return match;
40856
41757
  }
41758
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
41759
+ const callArguments = unwrappedExpression.arguments ?? [];
41760
+ if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
41761
+ allowGlobalReactNamespace: true,
41762
+ resolveNamedAliases: true
41763
+ })) {
41764
+ const callbackArgument = callArguments[0];
41765
+ if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
41766
+ const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
41767
+ return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
41768
+ }
41769
+ const callee = stripParenExpression(unwrappedExpression.callee);
41770
+ if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
41771
+ const helperFunction = resolveExactLocalFunction(callee, context.scopes);
41772
+ 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;
41773
+ const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
41774
+ for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
41775
+ const parameter = helperFunction.params[parameterIndex];
41776
+ const argument = callArguments[parameterIndex];
41777
+ if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
41778
+ const parameterSymbol = context.scopes.symbolFor(parameter);
41779
+ if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
41780
+ }
41781
+ return matchHydrationFunctionResult(helperFunction, context, {
41782
+ ...state,
41783
+ parameterValuesBySymbolId
41784
+ });
41785
+ }
41786
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
41787
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
41788
+ const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
41789
+ const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
40857
41790
  const nestedMatch = leftMatch ?? rightMatch;
40858
41791
  if (!nestedMatch) return null;
40859
- const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
40860
- if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
40861
- return nestedMatch;
41792
+ const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
41793
+ const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
41794
+ return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
41795
+ };
41796
+ const matchHydrationReturningStatement = (statement, context, state) => {
41797
+ if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
41798
+ if (isNodeOfType(statement, "IfStatement")) {
41799
+ const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
41800
+ const consequentValues = getReturnedValues(statement.consequent);
41801
+ const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
41802
+ if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
41803
+ return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
41804
+ }
41805
+ if (!isNodeOfType(statement, "BlockStatement")) return null;
41806
+ for (const childStatement of statement.body) {
41807
+ const match = matchHydrationReturningStatement(childStatement, context, state);
41808
+ if (match) return match;
41809
+ if (statementAlwaysExits(childStatement)) break;
41810
+ }
41811
+ return null;
40862
41812
  };
41813
+ const matchHydrationFunctionResult = (functionNode, context, state) => {
41814
+ if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
41815
+ state.visitedFunctionNodes.add(functionNode);
41816
+ const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
41817
+ state.visitedFunctionNodes.delete(functionNode);
41818
+ return match;
41819
+ };
41820
+ const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
41821
+ parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
41822
+ visitedFunctionNodes: /* @__PURE__ */ new Set(),
41823
+ visitedSymbolIds: /* @__PURE__ */ new Set()
41824
+ });
40863
41825
  const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
40864
41826
  const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
40865
41827
  if (!leftNode || !rightNode) return leftNode === rightNode;
@@ -41002,17 +41964,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
41002
41964
  const { predicateMatch, predicateNode } = conditionMatch;
41003
41965
  if (reportedNodes.has(predicateNode)) return;
41004
41966
  if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
41005
- const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
41967
+ const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
41006
41968
  if (!componentOrHookNode) return;
41007
41969
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
41008
- if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
41970
+ if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
41009
41971
  if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
41010
- const attribute = findEnclosingJsxAttribute(predicateNode);
41972
+ const attribute = findEnclosingJsxAttribute(conditionNode);
41011
41973
  if (!attribute || isEventHandlerAttribute(attribute)) return;
41012
41974
  }
41013
- if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
41014
- if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
41015
- const openingElement = findEnclosingJsxOpeningElement(predicateNode);
41975
+ if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
41976
+ if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
41977
+ const openingElement = findEnclosingJsxOpeningElement(conditionNode);
41016
41978
  if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
41017
41979
  if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
41018
41980
  if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
@@ -41389,12 +42351,12 @@ const noInitializeState = defineRule({
41389
42351
  tags: ["test-noise"],
41390
42352
  recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
41391
42353
  create: (context) => ({ CallExpression(node) {
41392
- if (!isUseEffect(node)) return;
42354
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
41393
42355
  const dependencies = node.arguments?.[1];
41394
42356
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
41395
42357
  const analysis = getProgramAnalysis(node);
41396
42358
  if (!analysis) return;
41397
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
42359
+ for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
41398
42360
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
41399
42361
  const stateName = getStateName(fact.stateDeclarator);
41400
42362
  context.report({
@@ -41752,7 +42714,8 @@ const noJsxElementType = defineRule({
41752
42714
  create: (context) => {
41753
42715
  let isJsxImported = false;
41754
42716
  const flaggedAnnotations = [];
41755
- const checkReturnType = (returnType) => {
42717
+ const collectComponentReturnType = (functionNode, returnType) => {
42718
+ if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
41756
42719
  const typeAnnotation = extractReturnTypeAnnotation(returnType);
41757
42720
  if (!typeAnnotation) return;
41758
42721
  if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
@@ -41762,19 +42725,16 @@ const noJsxElementType = defineRule({
41762
42725
  if (isJsxImportBinding(node)) isJsxImported = true;
41763
42726
  },
41764
42727
  FunctionDeclaration(node) {
41765
- checkReturnType(node.returnType);
42728
+ collectComponentReturnType(node, node.returnType);
41766
42729
  },
41767
42730
  ArrowFunctionExpression(node) {
41768
- checkReturnType(node.returnType);
42731
+ collectComponentReturnType(node, node.returnType);
41769
42732
  },
41770
42733
  FunctionExpression(node) {
41771
- checkReturnType(node.returnType);
42734
+ collectComponentReturnType(node, node.returnType);
41772
42735
  },
41773
42736
  TSDeclareFunction(node) {
41774
- checkReturnType(node.returnType);
41775
- },
41776
- TSMethodSignature(node) {
41777
- checkReturnType(node.returnType);
42737
+ collectComponentReturnType(node, node.returnType);
41778
42738
  },
41779
42739
  "Program:exit"() {
41780
42740
  if (isJsxImported) return;
@@ -43003,7 +43963,7 @@ const noMirrorPropEffect = defineRule({
43003
43963
  const setterElement = elements[1];
43004
43964
  if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
43005
43965
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
43006
- if (!isHookCall$2(declarator.init, "useState")) continue;
43966
+ if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
43007
43967
  const initializer = declarator.init.arguments?.[0];
43008
43968
  if (!initializer) continue;
43009
43969
  const propRootName = getPropRootName(initializer, propNames);
@@ -43021,7 +43981,7 @@ const noMirrorPropEffect = defineRule({
43021
43981
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
43022
43982
  const effectCall = unwrapDiscardedExpression(statement);
43023
43983
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
43024
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
43984
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
43025
43985
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
43026
43986
  const depsNode = effectCall.arguments[1];
43027
43987
  if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
@@ -43430,7 +44390,7 @@ const noMultiComp = defineRule({
43430
44390
  });
43431
44391
  //#endregion
43432
44392
  //#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
43433
- const collectUseRefBindingNames = (componentBody) => {
44393
+ const collectUseRefBindingNames = (componentBody, scopes) => {
43434
44394
  const useRefBindings = /* @__PURE__ */ new Set();
43435
44395
  if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
43436
44396
  for (const statement of componentBody.body ?? []) {
@@ -43438,7 +44398,7 @@ const collectUseRefBindingNames = (componentBody) => {
43438
44398
  for (const declarator of statement.declarations ?? []) {
43439
44399
  if (!isNodeOfType(declarator.id, "Identifier")) continue;
43440
44400
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
43441
- if (!isHookCall$2(declarator.init, "useRef")) continue;
44401
+ if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
43442
44402
  useRefBindings.add(declarator.id.name);
43443
44403
  }
43444
44404
  }
@@ -43473,12 +44433,12 @@ const noMutableInDeps = defineRule({
43473
44433
  create: (context) => {
43474
44434
  const checkComponent = (componentBody, componentParams = []) => {
43475
44435
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
43476
- const useRefBindingNames = collectUseRefBindingNames(componentBody);
44436
+ const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
43477
44437
  const localBindingNames = collectLocalBindingNames(componentBody);
43478
44438
  for (const param of componentParams) collectPatternNames(param, localBindingNames);
43479
44439
  walkAst(componentBody, (child) => {
43480
44440
  if (!isNodeOfType(child, "CallExpression")) return;
43481
- if (!isHookCall$2(child, HOOKS_WITH_DEPS)) return;
44441
+ if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
43482
44442
  if ((child.arguments?.length ?? 0) < 2) return;
43483
44443
  const depsNode = child.arguments[1];
43484
44444
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
@@ -45308,6 +46268,7 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
45308
46268
  "useMatchMedia",
45309
46269
  "useMediaJobProgress",
45310
46270
  "useMediaQuery",
46271
+ "useMediaQueryState",
45311
46272
  "useResizeObserver",
45312
46273
  "useVisibility",
45313
46274
  "useWindowSize"
@@ -45344,9 +46305,30 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
45344
46305
  if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
45345
46306
  return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
45346
46307
  };
45347
- const isExternalSubscriptionHookRef = (ref) => {
46308
+ const getLocalHookExternalStateProof = (analysis, ref) => {
46309
+ let hookFunction = resolveToFunction(ref);
46310
+ if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
46311
+ const definitionNode = definition.node;
46312
+ if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
46313
+ const initializer = stripParenExpression(definitionNode.init);
46314
+ if (!isNodeOfType(initializer, "CallExpression")) continue;
46315
+ const callee = stripParenExpression(initializer.callee);
46316
+ if (!isNodeOfType(callee, "Identifier")) continue;
46317
+ const calleeReference = getRef(analysis, callee);
46318
+ if (!calleeReference) continue;
46319
+ hookFunction = resolveToFunction(calleeReference);
46320
+ if (hookFunction) break;
46321
+ }
46322
+ if (!hookFunction) return null;
46323
+ const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
46324
+ if (returnedReferences.length === 0) return null;
46325
+ return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
46326
+ };
46327
+ const isExternalSubscriptionHookRef = (analysis, ref) => {
45348
46328
  const identifier = ref.identifier;
45349
46329
  if (!isNodeOfType(identifier, "Identifier")) return false;
46330
+ const localHookProof = getLocalHookExternalStateProof(analysis, ref);
46331
+ if (localHookProof !== null) return localHookProof;
45350
46332
  if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
45351
46333
  return Boolean(ref.resolved?.defs.some((def) => {
45352
46334
  const node = def.node;
@@ -45369,16 +46351,10 @@ const noPassDataToParent = defineRule({
45369
46351
  tags: ["test-noise"],
45370
46352
  recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
45371
46353
  create: (context) => {
45372
- const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
45373
- allowGlobalReactNamespace: true,
45374
- allowUnboundBareCalls: true
45375
- });
45376
- const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
45377
- allowGlobalReactNamespace: true,
45378
- allowUnboundBareCalls: true
45379
- });
46354
+ const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
46355
+ const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
45380
46356
  return { CallExpression(node) {
45381
- if (!isUseEffect(node)) return;
46357
+ if (!isReactUseEffectCall(node)) return;
45382
46358
  const analysis = getProgramAnalysis(node);
45383
46359
  if (!analysis) return;
45384
46360
  if (hasCleanup(analysis, node)) return;
@@ -45430,11 +46406,11 @@ const noPassDataToParent = defineRule({
45430
46406
  if (argumentRef && resolveToFunction(argumentRef)) return [];
45431
46407
  }
45432
46408
  return getDownstreamRefs(analysis, argument);
45433
- }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
46409
+ }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
45434
46410
  if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
45435
46411
  if (!argsUpstreamRefs.some((argRef) => {
45436
46412
  if (isUseStateIdentifier(argRef.identifier)) return false;
45437
- if (isExternalSubscriptionHookRef(argRef)) return false;
46413
+ if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
45438
46414
  if (isProp(analysis, argRef)) return false;
45439
46415
  if (isUseRefIdentifier(argRef.identifier)) return false;
45440
46416
  if (isRefCurrent(argRef)) return false;
@@ -45665,7 +46641,7 @@ const noPassLiveStateToParent = defineRule({
45665
46641
  tags: ["test-noise"],
45666
46642
  recommendation: "Move the state up to the parent (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes",
45667
46643
  create: (context) => ({ CallExpression(node) {
45668
- if (!isUseEffect(node)) return;
46644
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
45669
46645
  const analysis = getProgramAnalysis(node);
45670
46646
  if (!analysis) return;
45671
46647
  const effectFnRefs = getEffectFnRefs(analysis, node);
@@ -46080,7 +47056,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
46080
47056
  if (!isNodeOfType(element, "Identifier")) continue;
46081
47057
  const binding = findVariableInitializer(effectNode, element.name);
46082
47058
  if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
46083
- const calleeName = getCalleeName$2(binding.initializer);
47059
+ const calleeName = getCalleeName$1(binding.initializer);
46084
47060
  if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
46085
47061
  }
46086
47062
  return false;
@@ -46102,7 +47078,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
46102
47078
  if (!isNodeOfType(receiver, "Identifier")) return null;
46103
47079
  const binding = findVariableInitializer(callExpression, receiver.name);
46104
47080
  if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
46105
- if (getCalleeName$2(binding.initializer) !== "useRef") return null;
47081
+ if (getCalleeName$1(binding.initializer) !== "useRef") return null;
46106
47082
  const callbackArgument = binding.initializer.arguments?.[0];
46107
47083
  if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
46108
47084
  return isPropName(callbackArgument.name) ? callbackArgument.name : null;
@@ -46134,7 +47110,7 @@ const noPropCallbackInEffect = defineRule({
46134
47110
  return {
46135
47111
  ...propStackTracker.visitors,
46136
47112
  CallExpression(node) {
46137
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
47113
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
46138
47114
  const callback = getEffectCallback(node);
46139
47115
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
46140
47116
  const depsNode = node.arguments[1];
@@ -46998,6 +47974,69 @@ const noRedundantShouldComponentUpdate = defineRule({
46998
47974
  }
46999
47975
  });
47000
47976
  //#endregion
47977
+ //#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
47978
+ const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
47979
+ const expression = stripParenExpression(rawExpression);
47980
+ if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
47981
+ if (isNodeOfType(expression, "ConditionalExpression")) {
47982
+ if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
47983
+ return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
47984
+ }
47985
+ if (isNodeOfType(expression, "LogicalExpression")) {
47986
+ if (isNodeOfType(expression.left, "Literal")) {
47987
+ const isLeftTruthy = Boolean(expression.left.value);
47988
+ if (expression.operator === "&&" && !isLeftTruthy) return [];
47989
+ if (expression.operator === "||" && isLeftTruthy) return [];
47990
+ if (expression.operator === "??" && expression.left.value !== null) return [];
47991
+ }
47992
+ if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
47993
+ return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
47994
+ }
47995
+ if (isNodeOfType(expression, "SequenceExpression")) {
47996
+ const finalExpression = expression.expressions.at(-1);
47997
+ return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
47998
+ }
47999
+ if (isNodeOfType(expression, "CallExpression")) {
48000
+ if (!isReactApiCall(expression, "useCallback", scopes)) return [];
48001
+ const callback = expression.arguments[0];
48002
+ return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
48003
+ }
48004
+ if (!isNodeOfType(expression, "Identifier")) return [];
48005
+ const symbol = scopes.symbolFor(expression);
48006
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
48007
+ 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]));
48008
+ const initializer = getDirectConstInitializer(symbol);
48009
+ if (!initializer) return [];
48010
+ return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
48011
+ };
48012
+ const functionReturnsCleanupFunction = (functionExpression, scopes) => {
48013
+ if (!isFunctionLike$1(functionExpression)) return false;
48014
+ if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
48015
+ return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
48016
+ };
48017
+ const callbackReturnsCleanupFunction = (callback, scopes) => {
48018
+ return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
48019
+ };
48020
+ const noRefCallbackCleanupBeforeReact19 = defineRule({
48021
+ id: "no-ref-callback-cleanup-before-react-19",
48022
+ title: "Ref cleanup requires React 19",
48023
+ requires: ["react:18"],
48024
+ disabledWhen: ["react:19"],
48025
+ severity: "warn",
48026
+ 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.",
48027
+ create: (context) => ({ JSXAttribute(node) {
48028
+ if (getJsxAttributeName(node.name) !== "ref") return;
48029
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
48030
+ const callback = node.value.expression;
48031
+ if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
48032
+ if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
48033
+ context.report({
48034
+ node,
48035
+ 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."
48036
+ });
48037
+ } })
48038
+ });
48039
+ //#endregion
47001
48040
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
47002
48041
  const REPEATED_ANCESTOR_TYPES = new Set([
47003
48042
  "DoWhileStatement",
@@ -47964,7 +49003,7 @@ const noResetAllStateOnPropChange = defineRule({
47964
49003
  tags: ["test-noise"],
47965
49004
  recommendation: "Pass the prop as `key` so React resets the component for you when the prop changes, instead of clearing every state value by hand in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes",
47966
49005
  create: (context) => ({ CallExpression(node) {
47967
- if (!isUseEffect(node)) return;
49006
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
47968
49007
  const analysis = getProgramAnalysis(node);
47969
49008
  if (!analysis) return;
47970
49009
  const effectFnRefs = getEffectFnRefs(analysis, node);
@@ -48233,7 +49272,7 @@ const isTanStackServerFnHandlerCall = (node) => {
48233
49272
  if (node.callee.property.name !== "handler") return false;
48234
49273
  let currentNode = node.callee.object;
48235
49274
  while (isNodeOfType(currentNode, "CallExpression")) {
48236
- const calleeName = getCalleeName$2(currentNode);
49275
+ const calleeName = getCalleeName$1(currentNode);
48237
49276
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
48238
49277
  if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
48239
49278
  currentNode = currentNode.callee.object;
@@ -48734,7 +49773,7 @@ const noSelfUpdatingEffect = defineRule({
48734
49773
  create: (context) => {
48735
49774
  const checkFunctionScope = (functionBody) => {
48736
49775
  if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
48737
- const useStateBindings = collectUseStateBindings(functionBody);
49776
+ const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
48738
49777
  if (useStateBindings.length === 0) return;
48739
49778
  const setterNameToStateName = /* @__PURE__ */ new Map();
48740
49779
  for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
@@ -48743,7 +49782,7 @@ const noSelfUpdatingEffect = defineRule({
48743
49782
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
48744
49783
  const effectCall = unwrapDiscardedExpression(statement);
48745
49784
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
48746
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
49785
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
48747
49786
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
48748
49787
  const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
48749
49788
  if (dependencyStateNames.size === 0) continue;
@@ -48829,7 +49868,7 @@ const noSetStateInRender = defineRule({
48829
49868
  create: (context) => {
48830
49869
  const checkComponent = (componentBody) => {
48831
49870
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
48832
- const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
49871
+ const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
48833
49872
  if (setterNames.size === 0) return;
48834
49873
  for (const statement of componentBody.body ?? []) {
48835
49874
  const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
@@ -49078,10 +50117,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
49078
50117
  });
49079
50118
  return facts;
49080
50119
  };
49081
- const isEffectCallbackFunction = (functionNode) => {
50120
+ const isEffectCallbackFunction = (functionNode, scopes) => {
49082
50121
  const parent = functionNode.parent;
49083
50122
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
49084
- return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
50123
+ return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
49085
50124
  };
49086
50125
  const doesEffectCallbackReturnName = (effectCallback, name) => {
49087
50126
  if (!isFunctionLike$1(effectCallback)) return false;
@@ -49100,24 +50139,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
49100
50139
  const cleanupBindingName = getFunctionBindingName$1(functionNode);
49101
50140
  return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
49102
50141
  };
49103
- const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
50142
+ const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
49104
50143
  const cleanupBindingName = getFunctionBindingName$1(functionNode);
49105
50144
  if (cleanupBindingName === null) return false;
49106
50145
  let isReturnedFromEffect = false;
49107
50146
  walkAst(ownerScope, (child) => {
49108
50147
  if (isReturnedFromEffect) return false;
49109
- if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
50148
+ if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
49110
50149
  const effectCallback = getEffectCallback(child);
49111
50150
  if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
49112
50151
  });
49113
50152
  return isReturnedFromEffect;
49114
50153
  };
49115
- const isInsideEffectCleanupReturn = (node, ownerScope) => {
50154
+ const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
49116
50155
  let functionNode = findEnclosingFunction$1(node);
49117
50156
  while (functionNode) {
49118
50157
  const outerFunction = findEnclosingFunction$1(functionNode);
49119
- if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
49120
- if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
50158
+ if (outerFunction && isEffectCallbackFunction(outerFunction, scopes) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
50159
+ if (isReturnedFromAnyEffectInScope(functionNode, ownerScope, scopes)) return true;
49121
50160
  functionNode = outerFunction;
49122
50161
  }
49123
50162
  return false;
@@ -49153,10 +50192,10 @@ const noStaleTimerRef = defineRule({
49153
50192
  if (isShadowedTimerGlobal(node)) return;
49154
50193
  const { clearCalleeName, refName } = clearCall;
49155
50194
  const refBinding = findVariableInitializer(node, refName);
49156
- if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
50195
+ if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
49157
50196
  const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
49158
50197
  if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
49159
- if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
50198
+ if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
49160
50199
  if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
49161
50200
  context.report({
49162
50201
  node,
@@ -54960,7 +55999,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
54960
55999
  allReadsAreInSubHandlers = false;
54961
56000
  return;
54962
56001
  }
54963
- if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$2(subHandlerCall);
56002
+ if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
54964
56003
  });
54965
56004
  return {
54966
56005
  hasAnyRead,
@@ -54983,7 +56022,7 @@ const preferUseEffectEvent = defineRule({
54983
56022
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
54984
56023
  const effectCall = statement.expression;
54985
56024
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
54986
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
56025
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
54987
56026
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
54988
56027
  const depsNode = effectCall.arguments[1];
54989
56028
  if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
@@ -55015,11 +56054,11 @@ const preferUseEffectEvent = defineRule({
55015
56054
  });
55016
56055
  //#endregion
55017
56056
  //#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
55018
- const findUseEffectsInComponent = (componentBody) => {
56057
+ const findUseEffectsInComponent = (componentBody, scopes) => {
55019
56058
  const effectCalls = [];
55020
56059
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
55021
56060
  for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
55022
- if (isNodeOfType(child, "CallExpression") && isHookCall$2(child, EFFECT_HOOK_NAMES$1)) effectCalls.push(child);
56061
+ if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
55023
56062
  });
55024
56063
  return effectCalls;
55025
56064
  };
@@ -55222,7 +56261,7 @@ const preferUseSyncExternalStore = defineRule({
55222
56261
  };
55223
56262
  const checkComponent = (componentBody) => {
55224
56263
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
55225
- const useStateBindings = collectUseStateBindings(componentBody);
56264
+ const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
55226
56265
  if (useStateBindings.length === 0) return;
55227
56266
  const useStateInitializerByValueName = /* @__PURE__ */ new Map();
55228
56267
  for (const binding of useStateBindings) {
@@ -55235,7 +56274,7 @@ const preferUseSyncExternalStore = defineRule({
55235
56274
  }
55236
56275
  const setterNameToValueName = /* @__PURE__ */ new Map();
55237
56276
  for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
55238
- for (const effectCall of findUseEffectsInComponent(componentBody)) {
56277
+ for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
55239
56278
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
55240
56279
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
55241
56280
  const depsNode = effectCall.arguments[1];
@@ -55277,7 +56316,7 @@ const preferUseSyncExternalStore = defineRule({
55277
56316
  })).filter((candidate) => candidate.storeName !== null);
55278
56317
  if (snapshotBindings.length === 0) return;
55279
56318
  const reportedDeclarators = /* @__PURE__ */ new Set();
55280
- for (const effectCall of findUseEffectsInComponent(componentBody)) {
56319
+ for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
55281
56320
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
55282
56321
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
55283
56322
  const depsNode = effectCall.arguments[1];
@@ -55374,7 +56413,7 @@ const preferUseReducer = defineRule({
55374
56413
  create: (context) => {
55375
56414
  const reportCoUpdatedUseState = (body, componentName) => {
55376
56415
  if (!isNodeOfType(body, "BlockStatement")) return;
55377
- const bindings = collectUseStateBindings(body);
56416
+ const bindings = collectUseStateBindings(body, context.scopes);
55378
56417
  const setterNames = new Set(bindings.map((binding) => binding.setterName));
55379
56418
  if (setterNames.size < 5) return;
55380
56419
  const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
@@ -55590,7 +56629,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
55590
56629
  ]);
55591
56630
  const isQueryCacheSourceCall = (initializer) => {
55592
56631
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
55593
- const hookName = getCalleeName$2(initializer);
56632
+ const hookName = getCalleeName$1(initializer);
55594
56633
  if (!hookName) return false;
55595
56634
  return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
55596
56635
  };
@@ -55722,7 +56761,7 @@ const queryMutationMissingInvalidation = defineRule({
55722
56761
  },
55723
56762
  CallExpression(node) {
55724
56763
  if (!hasQueryReadUsage) {
55725
- const callName = getCalleeName$2(node);
56764
+ const callName = getCalleeName$1(node);
55726
56765
  if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
55727
56766
  }
55728
56767
  const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
@@ -56959,8 +57998,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
56959
57998
  destructureIndex: 1
56960
57999
  });
56961
58000
  //#endregion
58001
+ //#region src/plugin/utils/unwrap-return-expression.ts
58002
+ const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
58003
+ //#endregion
56962
58004
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
56963
58005
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
58006
+ const USE_CALLBACK_ONLY = new Set(["useCallback"]);
58007
+ const USE_STATE_ONLY = new Set(["useState"]);
58008
+ const REACT_API_CALL_OPTIONS = {
58009
+ allowGlobalReactNamespace: true,
58010
+ allowUnboundBareCalls: true,
58011
+ resolveNamedAliases: true
58012
+ };
58013
+ const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
58014
+ let readsDerivedSymbol = false;
58015
+ walkAst(expression, (node) => {
58016
+ if (readsDerivedSymbol) return false;
58017
+ if (node !== expression && isFunctionLike$1(node)) return false;
58018
+ if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
58019
+ });
58020
+ return readsDerivedSymbol;
58021
+ };
58022
+ const getStaticObjectPropertyName = (property) => {
58023
+ if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
58024
+ if (isNodeOfType(property.key, "Identifier")) return property.key.name;
58025
+ if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
58026
+ return null;
58027
+ };
58028
+ const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
58029
+ const isTransparentAssignmentTarget = (identifier) => {
58030
+ const expressionRoot = findTransparentExpressionRoot(identifier);
58031
+ const parent = expressionRoot.parent;
58032
+ return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
58033
+ };
56964
58034
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
56965
58035
  let readsCurrent = false;
56966
58036
  walkAst(argument, (child) => {
@@ -57012,6 +58082,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
57012
58082
  });
57013
58083
  return referenceCount > 0 && !nonAriaReferenceFound;
57014
58084
  };
58085
+ const isGlobalWindowMember = (context, node, propertyName) => {
58086
+ const member = stripParenExpression(node);
58087
+ if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
58088
+ const receiver = stripParenExpression(member.object);
58089
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
58090
+ };
58091
+ const getDirectWindowWidthSetter = (context, statement) => {
58092
+ const call = unwrapDiscardedExpression(statement);
58093
+ if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
58094
+ if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
58095
+ const argument = call.arguments[0];
58096
+ return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
58097
+ };
58098
+ const getResizeListenerHandler = (context, statement, methodName) => {
58099
+ const call = unwrapDiscardedExpression(statement);
58100
+ if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
58101
+ if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
58102
+ const eventName = call.arguments[0];
58103
+ const handler = call.arguments[1];
58104
+ if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
58105
+ return isNodeOfType(handler, "Identifier") ? handler : null;
58106
+ };
58107
+ const getCleanupResizeHandler = (context, statement) => {
58108
+ if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
58109
+ const cleanupStatements = getCallbackStatements(statement.argument);
58110
+ if (cleanupStatements.length !== 1) return null;
58111
+ return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
58112
+ };
58113
+ const findExactViewportState = (context, componentFunction, setterCall) => {
58114
+ if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
58115
+ const componentBody = componentFunction.body;
58116
+ if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
58117
+ const setterSymbol = context.scopes.symbolFor(setterCall.callee);
58118
+ if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
58119
+ const declarator = setterSymbol.declarationNode;
58120
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
58121
+ const stateIdentifier = declarator.id.elements?.[0];
58122
+ const setterIdentifier = declarator.id.elements?.[1];
58123
+ 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;
58124
+ const initializer = declarator.init.arguments?.[0];
58125
+ if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
58126
+ const stateSymbol = context.scopes.symbolFor(stateIdentifier);
58127
+ if (!stateSymbol) return null;
58128
+ const stateDerivedSymbolIds = new Set([stateSymbol.id]);
58129
+ let didAddDerivedSymbol = true;
58130
+ while (didAddDerivedSymbol) {
58131
+ didAddDerivedSymbol = false;
58132
+ for (const statement of componentBody.body ?? []) {
58133
+ if (!isNodeOfType(statement, "VariableDeclaration")) continue;
58134
+ for (const candidateDeclarator of statement.declarations ?? []) {
58135
+ if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
58136
+ const candidateInitializer = stripParenExpression(candidateDeclarator.init);
58137
+ if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
58138
+ if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
58139
+ const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
58140
+ if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
58141
+ stateDerivedSymbolIds.add(candidateSymbol.id);
58142
+ didAddDerivedSymbol = true;
58143
+ }
58144
+ }
58145
+ }
58146
+ }
58147
+ const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
58148
+ const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
58149
+ const symbol = context.scopes.symbolFor(identifier);
58150
+ if (!symbol) return false;
58151
+ if (visitedSymbolIds.has(symbol.id)) return true;
58152
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
58153
+ nextVisitedSymbolIds.add(symbol.id);
58154
+ let hasUnknownReference = false;
58155
+ walkAst(componentBody, (node) => {
58156
+ if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
58157
+ const referenceRoot = findTransparentExpressionRoot(node);
58158
+ const parent = referenceRoot.parent;
58159
+ if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
58160
+ if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
58161
+ hasUnknownReference = true;
58162
+ return false;
58163
+ });
58164
+ return !hasUnknownReference;
58165
+ };
58166
+ const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
58167
+ const symbol = context.scopes.symbolFor(identifier);
58168
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
58169
+ const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
58170
+ if (cachedVisibility) return cachedVisibility;
58171
+ if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
58172
+ if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
58173
+ const initializer = stripParenExpression(symbol.declarationNode.init);
58174
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
58175
+ nextVisitedSymbolIds.add(symbol.id);
58176
+ if (isNodeOfType(initializer, "Identifier")) {
58177
+ const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
58178
+ staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
58179
+ return visibility;
58180
+ }
58181
+ if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
58182
+ let visibility = "non-visible";
58183
+ for (const property of initializer.properties ?? []) {
58184
+ const propertyName = getStaticObjectPropertyName(property);
58185
+ if (!isNodeOfType(property, "Property") || !propertyName) {
58186
+ visibility = "unknown";
58187
+ break;
58188
+ }
58189
+ if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
58190
+ }
58191
+ staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
58192
+ return visibility;
58193
+ };
58194
+ let hasNonAriaReference = false;
58195
+ walkAst(componentBody, (node) => {
58196
+ if (hasNonAriaReference) return false;
58197
+ if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
58198
+ if (findEnclosingFunction$1(node) !== componentFunction) return;
58199
+ const parent = node.parent;
58200
+ if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
58201
+ let cursor = parent;
58202
+ while (cursor && cursor !== componentBody) {
58203
+ if (isFunctionLike$1(cursor)) return;
58204
+ if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
58205
+ if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
58206
+ return;
58207
+ }
58208
+ if (isNodeOfType(cursor, "JSXAttribute")) {
58209
+ if (isEventHandlerAttribute(cursor)) return;
58210
+ if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
58211
+ return;
58212
+ }
58213
+ if (isNodeOfType(cursor, "ReturnStatement")) {
58214
+ hasNonAriaReference = true;
58215
+ return;
58216
+ }
58217
+ cursor = cursor.parent;
58218
+ }
58219
+ });
58220
+ return hasNonAriaReference ? stateIdentifier.name : null;
58221
+ };
58222
+ const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
58223
+ if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
58224
+ if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
58225
+ const statements = getCallbackStatements(callback);
58226
+ if (statements.length !== 4) return false;
58227
+ const handlerDeclaration = statements[0];
58228
+ if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
58229
+ const handlerDeclarator = handlerDeclaration.declarations[0];
58230
+ if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
58231
+ const handlerStatements = getCallbackStatements(handlerDeclarator.init);
58232
+ if (handlerStatements.length !== 1) return false;
58233
+ const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
58234
+ const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
58235
+ const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
58236
+ const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
58237
+ if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
58238
+ const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
58239
+ if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
58240
+ if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
58241
+ const componentFunction = findEnclosingFunction$1(effectCall);
58242
+ if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
58243
+ return findExactViewportState(context, componentFunction, immediateSetter) !== null;
58244
+ };
57015
58245
  const renderingHydrationNoFlicker = defineRule({
57016
58246
  id: "rendering-hydration-no-flicker",
57017
58247
  title: "useEffect setState flashes on mount",
@@ -57024,7 +58254,14 @@ const renderingHydrationNoFlicker = defineRule({
57024
58254
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
57025
58255
  const callback = getEffectCallback(node);
57026
58256
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
57027
- const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
58257
+ if (isExactViewportSubscriptionEffect(context, node, callback)) {
58258
+ context.report({
58259
+ node,
58260
+ message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
58261
+ });
58262
+ return;
58263
+ }
58264
+ const bodyStatements = getCallbackStatements(callback);
57028
58265
  if (bodyStatements.length !== 1) return;
57029
58266
  const soleStatement = bodyStatements[0];
57030
58267
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
@@ -57187,6 +58424,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
57187
58424
  const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
57188
58425
  const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
57189
58426
  const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
58427
+ const FILE_READER_READ_METHOD_NAMES = new Set([
58428
+ "readAsArrayBuffer",
58429
+ "readAsBinaryString",
58430
+ "readAsDataURL",
58431
+ "readAsText"
58432
+ ]);
58433
+ const isGlobalFileReaderConstruction = (expression, context) => {
58434
+ if (!expression) return false;
58435
+ const unwrappedExpression = stripParenExpression(expression);
58436
+ if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
58437
+ return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
58438
+ };
58439
+ const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
58440
+ const readFunction = findEnclosingFunction$1(readCall);
58441
+ let latestValue = null;
58442
+ let latestStart = null;
58443
+ if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
58444
+ latestValue = readerSymbol.initializer;
58445
+ latestStart = readerSymbol.declarationNode.range[0];
58446
+ }
58447
+ for (const reference of readerSymbol.references) {
58448
+ if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
58449
+ const assignment = reference.identifier.parent;
58450
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
58451
+ latestValue = assignment.right;
58452
+ latestStart = reference.identifier.range[0];
58453
+ }
58454
+ return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
58455
+ };
58456
+ const resolveLoadingCompletionFunction = (expression, context) => {
58457
+ const directFunction = resolveExactLocalFunction(expression, context.scopes);
58458
+ if (directFunction) return directFunction;
58459
+ const unwrappedExpression = stripParenExpression(expression);
58460
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
58461
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
58462
+ const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
58463
+ if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
58464
+ const callback = initializer.arguments?.[0];
58465
+ return callback && isFunctionLike$1(callback) ? callback : null;
58466
+ };
58467
+ const isSetterBooleanCall = (node, setterSymbol, value, context) => {
58468
+ if (!isNodeOfType(node, "CallExpression")) return false;
58469
+ const callee = stripParenExpression(node.callee);
58470
+ const argument = node.arguments?.[0];
58471
+ const unwrappedArgument = argument ? stripParenExpression(argument) : null;
58472
+ return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
58473
+ };
58474
+ const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
58475
+ if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
58476
+ visitedFunctions.add(functionNode);
58477
+ let didClearLoadingState = false;
58478
+ walkAst(functionNode.body, (child) => {
58479
+ if (didClearLoadingState) return false;
58480
+ if (child !== functionNode.body && isFunctionLike$1(child)) return false;
58481
+ if (!isNodeOfType(child, "CallExpression")) return;
58482
+ if (isSetterBooleanCall(child, setterSymbol, false, context)) {
58483
+ didClearLoadingState = true;
58484
+ return false;
58485
+ }
58486
+ const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
58487
+ if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
58488
+ didClearLoadingState = true;
58489
+ return false;
58490
+ }
58491
+ });
58492
+ return didClearLoadingState;
58493
+ };
58494
+ const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
58495
+ const readFunction = findEnclosingFunction$1(readCall);
58496
+ if (!readFunction || !isFunctionLike$1(readFunction)) return null;
58497
+ let callback = null;
58498
+ let callbackStart = originStart;
58499
+ walkAst(readFunction.body, (child) => {
58500
+ if (child !== readFunction.body && isFunctionLike$1(child)) return false;
58501
+ if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=" || child.range[0] >= readCall.range[0] || child.range[0] <= callbackStart || !isNodeOfType(child.left, "MemberExpression") || getStaticPropertyName(child.left) !== propertyName) return;
58502
+ const receiver = stripParenExpression(child.left.object);
58503
+ if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
58504
+ callback = child.right;
58505
+ callbackStart = child.range[0];
58506
+ });
58507
+ return callback;
58508
+ };
58509
+ const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
58510
+ const readFunction = findEnclosingFunction$1(readCall);
58511
+ if (!readFunction || !isFunctionLike$1(readFunction)) return false;
58512
+ let didStartLoading = false;
58513
+ walkAst(readFunction.body, (child) => {
58514
+ if (didStartLoading) return false;
58515
+ if (child !== readFunction.body && isFunctionLike$1(child)) return false;
58516
+ if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
58517
+ if (isSetterBooleanCall(child, setterSymbol, true, context)) {
58518
+ didStartLoading = true;
58519
+ return false;
58520
+ }
58521
+ });
58522
+ return didStartLoading;
58523
+ };
58524
+ const setterTracksFileReader = (functionBody, setterSymbol, context) => {
58525
+ let didFindFileReaderLifecycle = false;
58526
+ walkAst(functionBody, (child) => {
58527
+ if (didFindFileReaderLifecycle) return false;
58528
+ if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
58529
+ const receiver = stripParenExpression(child.callee.object);
58530
+ if (!isNodeOfType(receiver, "Identifier")) return;
58531
+ const readerSymbol = context.scopes.symbolFor(receiver);
58532
+ if (!readerSymbol) return;
58533
+ const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
58534
+ if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
58535
+ const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
58536
+ const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
58537
+ const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
58538
+ const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
58539
+ if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
58540
+ didFindFileReaderLifecycle = true;
58541
+ return false;
58542
+ }
58543
+ });
58544
+ return didFindFileReaderLifecycle;
58545
+ };
57190
58546
  const hasAsyncLoadingWork = (fnBody, setterName) => {
57191
58547
  let found = false;
57192
58548
  walkAst(fnBody, (child) => {
@@ -57360,6 +58716,8 @@ const renderingUsetransitionLoading = defineRule({
57360
58716
  const fnBody = enclosingFunctionBody(node);
57361
58717
  if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
57362
58718
  if (fnBody && setterName) {
58719
+ const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
58720
+ if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
57363
58721
  if (setterEscapes(fnBody, setterName, node)) return;
57364
58722
  if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
57365
58723
  if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
@@ -57691,7 +59049,7 @@ const rerenderDependencies = defineRule({
57691
59049
  severity: "error",
57692
59050
  recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
57693
59051
  create: (context) => ({ CallExpression(node) {
57694
- if (!isHookCall$2(node, HOOKS_WITH_DEPS) || node.arguments.length < 2) return;
59052
+ if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
57695
59053
  const depsNode = node.arguments[1];
57696
59054
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
57697
59055
  for (const element of depsNode.elements ?? []) {
@@ -57946,7 +59304,7 @@ const rerenderLazyRefInit = defineRule({
57946
59304
  category: "Performance",
57947
59305
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
57948
59306
  create: (context) => ({ CallExpression(node) {
57949
- if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
59307
+ if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
57950
59308
  const initializer = stripParenExpression(node.arguments[0]);
57951
59309
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
57952
59310
  const isNewCall = isNodeOfType(initializer, "NewExpression");
@@ -58010,7 +59368,7 @@ const rerenderLazyStateInit = defineRule({
58010
59368
  category: "Performance",
58011
59369
  recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
58012
59370
  create: (context) => ({ CallExpression(node) {
58013
- if (!isHookCall$2(node, "useState") || !node.arguments?.length) return;
59371
+ if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
58014
59372
  const initializer = findEagerInitializerCall(node.arguments[0]);
58015
59373
  if (!initializer) return;
58016
59374
  const isConstructor = isNodeOfType(initializer, "NewExpression");
@@ -58773,11 +60131,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
58773
60131
  }
58774
60132
  return false;
58775
60133
  };
58776
- const collectEffectDependencyInfos = (componentBody, setterNames) => {
60134
+ const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
58777
60135
  const effectInfos = [];
58778
60136
  walkAst(componentBody, (child) => {
58779
60137
  if (!isNodeOfType(child, "CallExpression")) return;
58780
- if (!isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
60138
+ if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
58781
60139
  const dependencyNames = /* @__PURE__ */ new Set();
58782
60140
  for (const argument of child.arguments ?? []) {
58783
60141
  if (!isNodeOfType(argument, "ArrayExpression")) continue;
@@ -58833,13 +60191,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
58833
60191
  });
58834
60192
  return effectInfos;
58835
60193
  };
58836
- const collectCustomHookArgumentNames = (componentBody) => {
60194
+ const collectCustomHookArgumentNames = (componentBody, scopes) => {
58837
60195
  const argumentNames = /* @__PURE__ */ new Set();
58838
60196
  walkAst(componentBody, (child) => {
58839
60197
  if (!isNodeOfType(child, "CallExpression")) return;
58840
60198
  if (!isNodeOfType(child.callee, "Identifier")) return;
58841
60199
  const calleeName = child.callee.name;
58842
60200
  if (!isReactHookName(calleeName)) return;
60201
+ if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
58843
60202
  if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
58844
60203
  if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
58845
60204
  for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
@@ -58880,21 +60239,21 @@ const rerenderStateOnlyInHandlers = defineRule({
58880
60239
  create: (context) => {
58881
60240
  const checkComponent = (componentBody) => {
58882
60241
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
58883
- const bindings = collectUseStateBindings(componentBody);
60242
+ const bindings = collectUseStateBindings(componentBody, context.scopes);
58884
60243
  if (bindings.length === 0) return;
58885
60244
  if (collectRenderReachableExpressions(componentBody).length === 0) return;
58886
- const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
60245
+ const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
58887
60246
  const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
58888
- const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
60247
+ const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
58889
60248
  if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
58890
60249
  const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
58891
60250
  const setterNames = new Set(bindings.map((binding) => binding.setterName));
58892
- const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
60251
+ const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
58893
60252
  const selfEchoValueNames = /* @__PURE__ */ new Set();
58894
60253
  for (const binding of bindings) if (effectInfos.some((effectInfo) => effectInfo.dependencyNames.has(binding.valueName) && effectInfo.synchronouslyCalledFunctionNames.has(binding.setterName) && !effectInfo.payloadReadNames.has(binding.valueName) && !effectInfo.nestedCallbackCalledFunctionNames.has(binding.setterName))) selfEchoValueNames.add(binding.valueName);
58895
60254
  const effectConsumedNames = /* @__PURE__ */ new Set();
58896
60255
  for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
58897
- for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
60256
+ for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
58898
60257
  for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
58899
60258
  const calledSetterNames = /* @__PURE__ */ new Set();
58900
60259
  walkAst(componentBody, (child) => {
@@ -66795,7 +68154,7 @@ const declarationAwaitsGate = (declaration, context) => {
66795
68154
  if (!isNodeOfType(argument, "CallExpression")) continue;
66796
68155
  if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
66797
68156
  if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
66798
- const calleeName = getCalleeName$2(argument);
68157
+ const calleeName = getCalleeName$1(argument);
66799
68158
  if (!calleeName) continue;
66800
68159
  if (isAuthGuardName(calleeName)) return true;
66801
68160
  const [leadingToken] = tokenizeIdentifierWords(calleeName);
@@ -67435,7 +68794,7 @@ const walkServerFnChain = (outerNode) => {
67435
68794
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
67436
68795
  let currentNode = stripParenExpression(outerNode.callee.object);
67437
68796
  while (isNodeOfType(currentNode, "CallExpression")) {
67438
- const calleeName = getCalleeName$2(currentNode);
68797
+ const calleeName = getCalleeName$1(currentNode);
67439
68798
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
67440
68799
  result.isServerFnChain = true;
67441
68800
  const optionsArgument = currentNode.arguments?.[0];
@@ -71567,6 +72926,17 @@ const reactDoctorRules = [
71567
72926
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
71568
72927
  }
71569
72928
  },
72929
+ {
72930
+ key: "react-doctor/no-ref-callback-cleanup-before-react-19",
72931
+ id: "no-ref-callback-cleanup-before-react-19",
72932
+ source: "react-doctor",
72933
+ originallyExternal: false,
72934
+ rule: {
72935
+ ...noRefCallbackCleanupBeforeReact19,
72936
+ framework: "global",
72937
+ category: "Bugs"
72938
+ }
72939
+ },
71570
72940
  {
71571
72941
  key: "react-doctor/no-ref-current-in-render",
71572
72942
  id: "no-ref-current-in-render",