oxlint-plugin-react-doctor 0.7.9-dev.4f3848b → 0.7.9-dev.51ecd7b

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 (2) hide show
  1. package/dist/index.js +510 -338
  2. 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,38 +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, allowPatternBinding = false) => {
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")) return null;
1246
- if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
1247
- visitedSymbolIds.add(symbol.id);
1248
- const initializer = stripParenExpression(symbol.initializer);
1249
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
1250
- symbol = scopes.symbolFor(initializer);
1251
- }
1252
- return symbol;
1253
- };
1254
- //#endregion
1255
1340
  //#region src/plugin/utils/resolve-exact-local-function.ts
1256
1341
  const resolveExactLocalFunction = (expression, scopes) => {
1257
1342
  const unwrappedExpression = stripParenExpression(expression);
@@ -1297,9 +1382,17 @@ const getRootIdentifier$1 = (node, options) => {
1297
1382
  //#region src/plugin/utils/get-root-identifier-name.ts
1298
1383
  const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
1299
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
1300
1393
  //#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
1301
- const STABLE_HANDLER_HOOK_NAMES = new Set([
1302
- "useCallback",
1394
+ const REACT_STABLE_HANDLER_HOOK_NAMES = new Set(["useCallback", "useEffectEvent"]);
1395
+ const CUSTOM_STABLE_HANDLER_HOOK_NAMES = new Set([
1303
1396
  "useEffectEvent",
1304
1397
  "useEvent",
1305
1398
  "useEventCallback",
@@ -1308,21 +1401,21 @@ const STABLE_HANDLER_HOOK_NAMES = new Set([
1308
1401
  ]);
1309
1402
  const THROTTLED_HANDLER_HOOK_PATTERN = /^use\w*(?:Throttle|Debounce)/i;
1310
1403
  const isThrottledHandlerHookCall = (callNode) => {
1311
- const calleeName = getCalleeName$2(callNode);
1404
+ const calleeName = getCalleeName$1(callNode);
1312
1405
  return calleeName !== null && THROTTLED_HANDLER_HOOK_PATTERN.test(calleeName);
1313
1406
  };
1314
- const isEmptyDepsUseMemoCall = (callNode) => {
1315
- if (!isHookCall$2(callNode, "useMemo")) return false;
1407
+ const isEmptyDepsUseMemoCall = (callNode, scopes) => {
1408
+ if (!isReactHookCall(callNode, "useMemo", scopes)) return false;
1316
1409
  const memoDepsNode = callNode.arguments?.[1];
1317
1410
  return isNodeOfType(memoDepsNode, "ArrayExpression") && (memoDepsNode.elements?.length ?? 0) === 0;
1318
1411
  };
1319
- const isStableHandlerInitializer = (initializer) => {
1320
- 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);
1321
1414
  return isNodeOfType(initializer, "MemberExpression") && isNodeOfType(initializer.property, "Identifier") && initializer.property.name === "current";
1322
1415
  };
1323
- const isStableRefReceiverDep = (referenceNode, receiverDepName) => {
1416
+ const isStableRefReceiverDep = (referenceNode, receiverDepName, scopes) => {
1324
1417
  const receiverBinding = findVariableInitializer(referenceNode, receiverDepName);
1325
- return Boolean(receiverBinding?.initializer && isHookCall$2(receiverBinding.initializer, "useRef"));
1418
+ return Boolean(receiverBinding?.initializer && isReactHookCall(receiverBinding.initializer, "useRef", scopes));
1326
1419
  };
1327
1420
  const advancedEventHandlerRefs = defineRule({
1328
1421
  id: "advanced-event-handler-refs",
@@ -1332,7 +1425,7 @@ const advancedEventHandlerRefs = defineRule({
1332
1425
  category: "Performance",
1333
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.",
1334
1427
  create: (context) => ({ CallExpression(node) {
1335
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
1428
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
1336
1429
  if ((node.arguments?.length ?? 0) < 2) return;
1337
1430
  const callback = getEffectCallback(node);
1338
1431
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -1356,8 +1449,8 @@ const advancedEventHandlerRefs = defineRule({
1356
1449
  });
1357
1450
  if (!registeredHandlerName) return;
1358
1451
  const handlerBinding = findVariableInitializer(node, registeredHandlerName);
1359
- if (handlerBinding?.initializer && isStableHandlerInitializer(handlerBinding.initializer)) return;
1360
- 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;
1361
1454
  context.report({
1362
1455
  node,
1363
1456
  message: `useEffect re-adds the "${registeredHandlerName}" listener every time the handler changes.`
@@ -1844,15 +1937,6 @@ const flattenJsxName$1 = (node) => {
1844
1937
  return null;
1845
1938
  };
1846
1939
  //#endregion
1847
- //#region src/plugin/utils/get-static-property-name.ts
1848
- const getStaticPropertyName = (memberExpression) => {
1849
- const property = memberExpression.property;
1850
- if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
1851
- if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
1852
- if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
1853
- return null;
1854
- };
1855
- //#endregion
1856
1940
  //#region src/plugin/utils/is-generated-image-renderer-call.ts
1857
1941
  const GENERATED_IMAGE_RENDERER_MODULES = [
1858
1942
  "next/og",
@@ -4860,23 +4944,6 @@ const containsDirectAwait = (node) => {
4860
4944
  return foundAwait;
4861
4945
  };
4862
4946
  //#endregion
4863
- //#region src/plugin/utils/get-static-property-key-name.ts
4864
- const getStaticPropertyKeyName = (node, options = {}) => {
4865
- if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition") && !isNodeOfType(node, "MemberExpression")) return null;
4866
- const key = isNodeOfType(node, "MemberExpression") ? node.property : node.key;
4867
- if (node.computed) {
4868
- if (options.allowComputedString && isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value;
4869
- if (options.allowComputedString && isNodeOfType(key, "TemplateLiteral") && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? key.quasis[0]?.value.raw ?? null;
4870
- return null;
4871
- }
4872
- if (isNodeOfType(key, "Identifier")) return key.name;
4873
- if (isNodeOfType(key, "Literal")) {
4874
- if (typeof key.value === "string") return key.value;
4875
- if (options.stringifyNonStringLiterals) return String(key.value);
4876
- }
4877
- return null;
4878
- };
4879
- //#endregion
4880
4947
  //#region src/plugin/utils/get-destructured-binding-property-name.ts
4881
4948
  const getDestructuredBindingPropertyName = (bindingIdentifier) => {
4882
4949
  let bindingNode = bindingIdentifier;
@@ -8941,65 +9008,6 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
8941
9008
  }
8942
9009
  };
8943
9010
  //#endregion
8944
- //#region src/plugin/utils/is-react-api-call.ts
8945
- const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
8946
- const isImportedFromReact = (symbol) => {
8947
- if (symbol.kind !== "import") return false;
8948
- const importDeclaration = symbol.declarationNode.parent;
8949
- return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
8950
- };
8951
- const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
8952
- if (!isNodeOfType(identifier, "Identifier")) return false;
8953
- const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
8954
- if (!symbol || !isImportedFromReact(symbol)) return false;
8955
- const importedName = getImportedName(symbol.declarationNode);
8956
- return Boolean(importedName && includesApiName(apiNames, importedName));
8957
- };
8958
- const isReactNamespaceImport = (identifier, scopes) => {
8959
- const symbol = resolveConstIdentifierAlias(identifier, scopes);
8960
- if (!symbol || !isImportedFromReact(symbol)) return false;
8961
- return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
8962
- };
8963
- const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
8964
- if (!isNodeOfType(receiver, "Identifier")) return false;
8965
- if (isReactNamespaceImport(receiver, scopes)) return true;
8966
- return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
8967
- };
8968
- const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) => {
8969
- const symbol = scopes.symbolFor(identifier);
8970
- if (!symbol || symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
8971
- const pattern = symbol.declarationNode.id;
8972
- if (!isNodeOfType(pattern, "ObjectPattern")) return false;
8973
- for (const property of pattern.properties) {
8974
- if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
8975
- const propertyName = getStaticPropertyKeyName(property);
8976
- return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
8977
- }
8978
- return false;
8979
- };
8980
- const isReactApiCall = (node, apiNames, scopes, options = {}) => {
8981
- if (!isNodeOfType(node, "CallExpression")) return false;
8982
- return isReactApiCallee(node.callee, apiNames, scopes, options, /* @__PURE__ */ new Set());
8983
- };
8984
- const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds) => {
8985
- const callee = stripParenExpression(rawCallee);
8986
- 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));
8987
- if (isNodeOfType(callee, "Identifier")) {
8988
- if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
8989
- if (options.resolveNamedAliases && isDestructuredReactApiBinding(callee, apiNames, scopes, options)) return true;
8990
- if (options.resolveConditionalAliases) {
8991
- const symbol = scopes.symbolFor(callee);
8992
- if (symbol?.kind === "const" && symbol.initializer && !visitedSymbolIds.has(symbol.id)) {
8993
- visitedSymbolIds.add(symbol.id);
8994
- return isReactApiCallee(symbol.initializer, apiNames, scopes, options, visitedSymbolIds);
8995
- }
8996
- }
8997
- return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
8998
- }
8999
- if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
9000
- return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
9001
- };
9002
- //#endregion
9003
9011
  //#region src/plugin/utils/is-proven-browser-api-receiver.ts
9004
9012
  const DOM_EVENT_TARGET_TYPE_NAMES = new Set([
9005
9013
  "AbortSignal",
@@ -13883,6 +13891,7 @@ const walkInsideStatementBlocks = (node, visitor) => {
13883
13891
  };
13884
13892
  //#endregion
13885
13893
  //#region src/plugin/rules/state-and-effects/utils/is-subscribe-like-call-expression.ts
13894
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
13886
13895
  const getSubscribeLikeMethodName = (node) => {
13887
13896
  if (!isNodeOfType(node, "CallExpression")) return null;
13888
13897
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
@@ -13893,6 +13902,11 @@ const isSubscribeLikeCallExpression = (node) => {
13893
13902
  const methodName = getSubscribeLikeMethodName(node);
13894
13903
  return methodName !== null && SUBSCRIPTION_METHOD_NAMES.has(methodName);
13895
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;
13896
13910
  const isCleanupReturningSubscribeLikeCallExpression = (node) => {
13897
13911
  const methodName = getSubscribeLikeMethodName(node);
13898
13912
  if (methodName === null || !CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES.has(methodName)) return false;
@@ -13945,7 +13959,6 @@ const isNodeReachableWithinFunction = (node, context) => {
13945
13959
  };
13946
13960
  //#endregion
13947
13961
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
13948
- const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
13949
13962
  const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
13950
13963
  const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
13951
13964
  const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
@@ -13955,10 +13968,6 @@ const RESOURCE_NOUN_BY_KIND = {
13955
13968
  socket: "connection"
13956
13969
  };
13957
13970
  const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
13958
- const isSubscribeOrObserveCall = (node) => {
13959
- if (isSubscribeLikeCallExpression(node)) return true;
13960
- return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
13961
- };
13962
13971
  const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
13963
13972
  if (!expression) return null;
13964
13973
  const unwrappedExpression = stripParenExpression(expression);
@@ -14060,12 +14069,13 @@ const findSubscribeLikeUsages = (callback, context) => {
14060
14069
  });
14061
14070
  return;
14062
14071
  }
14063
- 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) {
14064
14074
  const registrationDetails = getCallRegistrationDetails(child, context);
14065
14075
  usages.push({
14066
14076
  kind: "subscribe",
14067
14077
  node: child,
14068
- resourceName: child.callee.property.name,
14078
+ resourceName: subscribeOrObserveMethodName,
14069
14079
  handleKey: findAssignedResourceKey(child, context),
14070
14080
  ...registrationDetails
14071
14081
  });
@@ -14596,7 +14606,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
14596
14606
  walkAst(componentFunction.body, (child) => {
14597
14607
  if (didFindUnmountCleanup) return false;
14598
14608
  if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction) return;
14599
- if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
14609
+ if (!isReactHookCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
14600
14610
  const dependencyList = child.arguments?.[1];
14601
14611
  if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
14602
14612
  const cleanupCallback = getEffectCallback(child);
@@ -15027,7 +15037,7 @@ const getReleaseVerbName = (node) => {
15027
15037
  const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
15028
15038
  const releaseFunction = findEnclosingFunction$1(releaseReceiver);
15029
15039
  const usageFunction = findEnclosingFunction$1(usage.node);
15030
- if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
15040
+ if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction, context) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
15031
15041
  const controllerKey = getListenerAbortControllerKey(usage, context);
15032
15042
  const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
15033
15043
  if (controllerKey === null || refCurrentKey === null) return false;
@@ -15174,7 +15184,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15174
15184
  return true;
15175
15185
  };
15176
15186
  const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
15177
- const isReturnedEffectCleanupFunction = (functionNode) => {
15187
+ const isReturnedEffectCleanupFunction = (functionNode, context) => {
15178
15188
  let currentNode = functionNode;
15179
15189
  let parentNode = currentNode.parent;
15180
15190
  while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
@@ -15183,10 +15193,10 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
15183
15193
  }
15184
15194
  const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
15185
15195
  const effectCall = effectCallback?.parent;
15186
- 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));
15187
15197
  };
15188
15198
  const isPotentiallyReachableFunction = (functionNode, context) => {
15189
- if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
15199
+ if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
15190
15200
  const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
15191
15201
  if (!bindingIdentifier) return false;
15192
15202
  const symbol = context.scopes.symbolFor(bindingIdentifier);
@@ -15683,7 +15693,7 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
15683
15693
  return false;
15684
15694
  }
15685
15695
  }
15686
- 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))) {
15687
15697
  const registrationDetails = getCallRegistrationDetails(child, context);
15688
15698
  const subscriptionUsage = {
15689
15699
  kind: "subscribe",
@@ -15891,7 +15901,7 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
15891
15901
  if (!isFunctionLike$1(functionNode)) return false;
15892
15902
  const functionRoot = findTransparentExpressionRoot(functionNode);
15893
15903
  const callbackCall = functionRoot.parent;
15894
- 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;
15895
15905
  const parentNode = functionNode.parent;
15896
15906
  if (isDirectJsxEventHandlerValue(functionNode)) return true;
15897
15907
  if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
@@ -15927,12 +15937,12 @@ const effectNeedsCleanup = defineRule({
15927
15937
  };
15928
15938
  return {
15929
15939
  CallExpression(node) {
15930
- if (isHookCall$2(node, "useCallback")) {
15940
+ if (isReactHookCall(node, "useCallback", context.scopes)) {
15931
15941
  const retainedCallback = getEffectCallback(node);
15932
15942
  if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
15933
15943
  return;
15934
15944
  }
15935
- if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
15945
+ if (!isReactHookCall(node, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
15936
15946
  const callback = getEffectCallback(node);
15937
15947
  if (!callback) return;
15938
15948
  const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
@@ -15940,7 +15950,7 @@ const effectNeedsCleanup = defineRule({
15940
15950
  const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
15941
15951
  if (!firstUsage) return;
15942
15952
  const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
15943
- const hookName = getCalleeName$2(node) ?? "effect";
15953
+ const hookName = getCalleeName$1(node) ?? "effect";
15944
15954
  context.report({
15945
15955
  node,
15946
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.`
@@ -17283,7 +17293,38 @@ const symbolHasStableImportedAlias = (symbol, scopes) => {
17283
17293
  const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
17284
17294
  return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
17285
17295
  };
17286
- 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);
17287
17328
  //#endregion
17288
17329
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
17289
17330
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -18466,7 +18507,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
18466
18507
  if (!isUsed) continue;
18467
18508
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
18468
18509
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
18469
- if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || !isUnstableInitializer(rootSymbol.initializer)) continue;
18510
+ if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || symbolHasStableValue(rootSymbol, context.scopes) || !isUnstableInitializer(rootSymbol.initializer)) continue;
18470
18511
  context.report({
18471
18512
  node: reportNode,
18472
18513
  message: buildUnstableDepMessage(hookName, declaredKey)
@@ -18782,7 +18823,7 @@ const flattenCalleeName = (callee) => {
18782
18823
  const PRAGMA = "React";
18783
18824
  const isReactFunctionCall = (node, expectedCall) => {
18784
18825
  if (!isNodeOfType(node, "CallExpression")) return false;
18785
- if (getCalleeName$2(node) !== expectedCall) return false;
18826
+ if (getCalleeName$1(node) !== expectedCall) return false;
18786
18827
  if (isNodeOfType(node.callee, "MemberExpression")) {
18787
18828
  const receiver = stripParenExpression(node.callee.object);
18788
18829
  return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
@@ -18980,10 +19021,14 @@ const hookUseState = defineRule({
18980
19021
  create: (context) => {
18981
19022
  const { allowDestructuredState } = resolveSettings$40(context.settings);
18982
19023
  return { CallExpression(node) {
18983
- if (!isReactFunctionCall(node, "useState")) return;
19024
+ const callExpressionNode = node;
19025
+ if (!isReactFunctionCall(callExpressionNode, "useState")) return;
19026
+ const expressionRoot = findTransparentExpressionRoot(callExpressionNode);
19027
+ const expressionParent = expressionRoot.parent;
19028
+ if (isNodeOfType(expressionParent, "ReturnStatement")) return;
19029
+ if (isNodeOfType(expressionParent, "ArrowFunctionExpression") && expressionParent.body === expressionRoot) return;
18984
19030
  const parent = node.parent;
18985
19031
  if (!parent) return;
18986
- if (isNodeOfType(parent, "ReturnStatement")) return;
18987
19032
  if (!isNodeOfType(parent, "VariableDeclarator")) {
18988
19033
  context.report({
18989
19034
  node,
@@ -19091,8 +19136,8 @@ const hooksNoNanInDeps = defineRule({
19091
19136
  severity: "warn",
19092
19137
  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.",
19093
19138
  create: (context) => ({ CallExpression(node) {
19094
- if (!isHookCall$2(node, HOOKS_WITH_DEP_ARRAY)) return;
19095
- const depsIndex = getCalleeName$2(node) === "useImperativeHandle" ? 2 : 1;
19139
+ if (!isReactHookCall(node, HOOKS_WITH_DEP_ARRAY, context.scopes)) return;
19140
+ const depsIndex = isReactHookCall(node, "useImperativeHandle", context.scopes) ? 2 : 1;
19096
19141
  const depsArgument = node.arguments[depsIndex];
19097
19142
  if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
19098
19143
  for (const element of depsArgument.elements) {
@@ -20263,7 +20308,7 @@ const isImportedSelectAtom = (callExpression) => {
20263
20308
  const isDeferredCallbackPosition$1 = (functionNode) => {
20264
20309
  const parent = functionNode.parent;
20265
20310
  if (isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === functionNode) {
20266
- const hookName = getCalleeName$2(parent);
20311
+ const hookName = getCalleeName$1(parent);
20267
20312
  if (hookName && MEMOIZING_HOOK_NAMES$1.has(hookName)) return true;
20268
20313
  if (hookName && EFFECT_HOOK_NAMES$1.has(hookName) && Boolean(parent.arguments?.[1])) return true;
20269
20314
  }
@@ -30334,7 +30379,7 @@ const DEFERRING_CALLEE_NAMES$1 = new Set([
30334
30379
  "on",
30335
30380
  "once"
30336
30381
  ]);
30337
- const getCalleeName$1 = (callee) => {
30382
+ const getCalleeName = (callee) => {
30338
30383
  if (!callee) return null;
30339
30384
  if (isNodeOfType(callee, "Identifier")) return callee.name;
30340
30385
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
@@ -30346,11 +30391,11 @@ const isDeferredCallbackPosition = (expression) => {
30346
30391
  const parent = parentOf(expression);
30347
30392
  if (!parent) return false;
30348
30393
  if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
30349
- const name = getCalleeName$1(parent.callee);
30394
+ const name = getCalleeName(parent.callee);
30350
30395
  if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
30351
30396
  }
30352
30397
  if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
30353
- const name = getCalleeName$1(parent.callee);
30398
+ const name = getCalleeName(parent.callee);
30354
30399
  if (name && (name.endsWith("Observer") || name === "Promise")) return true;
30355
30400
  }
30356
30401
  if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
@@ -30770,14 +30815,6 @@ const isHookCallee$1 = (analysis, node, hookName) => {
30770
30815
  if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
30771
30816
  return false;
30772
30817
  };
30773
- const isUseEffect = (node) => {
30774
- if (!node || !isNodeOfType(node, "CallExpression")) return false;
30775
- const callee = node.callee;
30776
- if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
30777
- if (!isNodeOfType(callee, "MemberExpression")) return false;
30778
- const receiver = stripParenExpression(callee.object);
30779
- return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
30780
- };
30781
30818
  const getEffectFn = (analysis, node) => {
30782
30819
  if (!isNodeOfType(node, "CallExpression")) return null;
30783
30820
  const fn = node.arguments?.[0];
@@ -32239,6 +32276,7 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
32239
32276
  sourceReferences,
32240
32277
  isDeferred: frame.isDeferred,
32241
32278
  isRenderKnownCopy,
32279
+ isSynchronousRenderValue: !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue,
32242
32280
  matchesStateInitializer: doesMatchStateInitializer,
32243
32281
  resetsSourceState: false
32244
32282
  });
@@ -32254,27 +32292,71 @@ const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFile
32254
32292
  });
32255
32293
  };
32256
32294
  //#endregion
32295
+ //#region src/plugin/rules/state-and-effects/utils/has-deferred-or-external-effect-work.ts
32296
+ const DEFERRED_MEMBER_NAMES = new Set([
32297
+ "catch",
32298
+ "finally",
32299
+ "then"
32300
+ ]);
32301
+ const hasDeferredOrExternalEffectWork = (analysis, effectNode, scopes) => {
32302
+ const effectFunction = getEffectFn(analysis, effectNode);
32303
+ if (!effectFunction) return false;
32304
+ if (containsFetchCall(effectFunction, { stopAtFunctionBoundary: true })) return true;
32305
+ const effectInvokedFunctions = collectEffectInvokedFunctions(effectFunction);
32306
+ let didFindDeferredOrExternalWork = false;
32307
+ walkAst(effectFunction, (child) => {
32308
+ if (didFindDeferredOrExternalWork) return false;
32309
+ if (child !== effectFunction && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
32310
+ if (isNodeOfType(child, "AssignmentExpression")) {
32311
+ const assignmentTarget = child.left;
32312
+ if ((isNodeOfType(assignmentTarget, "MemberExpression") ? getStaticPropertyName(assignmentTarget) : null)?.startsWith("on") && isFunctionLike$1(child.right)) {
32313
+ didFindDeferredOrExternalWork = true;
32314
+ return false;
32315
+ }
32316
+ }
32317
+ if (!isNodeOfType(child, "CallExpression")) return;
32318
+ if (isSubscribeOrObserveCallExpression(child)) {
32319
+ didFindDeferredOrExternalWork = true;
32320
+ return false;
32321
+ }
32322
+ const localFunction = resolveExactLocalFunction(child.callee, scopes);
32323
+ if (isFunctionLike$1(localFunction) && localFunction.async) {
32324
+ didFindDeferredOrExternalWork = true;
32325
+ return false;
32326
+ }
32327
+ const callee = child.callee;
32328
+ if (isNodeOfType(callee, "Identifier") && TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES.has(callee.name)) {
32329
+ didFindDeferredOrExternalWork = true;
32330
+ return false;
32331
+ }
32332
+ const memberName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
32333
+ if (memberName && DEFERRED_MEMBER_NAMES.has(memberName)) {
32334
+ didFindDeferredOrExternalWork = true;
32335
+ return false;
32336
+ }
32337
+ });
32338
+ return didFindDeferredOrExternalWork;
32339
+ };
32340
+ //#endregion
32257
32341
  //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
32258
32342
  const noAdjustStateOnPropChange = defineRule({
32259
32343
  id: "no-adjust-state-on-prop-change",
32260
- title: "State synced to a prop inside an effect",
32344
+ title: "State adjusted after a prop changes",
32261
32345
  severity: "warn",
32262
32346
  tags: ["test-noise"],
32263
- recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
32347
+ 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",
32264
32348
  create: (context) => ({ CallExpression(node) {
32265
- if (!isReactApiCall(node, "useEffect", context.scopes, {
32266
- allowGlobalReactNamespace: true,
32267
- allowUnboundBareCalls: true,
32268
- resolveConditionalAliases: true,
32269
- resolveNamedAliases: true
32270
- })) return;
32349
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
32271
32350
  const analysis = getProgramAnalysis(node);
32272
32351
  if (!analysis) return;
32273
32352
  const dependencyReferences = getEffectDepsRefs(analysis, node);
32274
32353
  if (!dependencyReferences) return;
32275
32354
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
32276
- for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
32277
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
32355
+ const facts = collectEffectStateWriteFacts(analysis, context, node, context.filename);
32356
+ if (hasCleanup(analysis, node) || hasDeferredOrExternalEffectWork(analysis, node, context.scopes) || facts.some((fact) => fact.isDeferred)) return;
32357
+ for (const fact of facts) {
32358
+ if (!fact.isSynchronousRenderValue || fact.resetsSourceState) continue;
32359
+ if (fact.sourceReferences.flatMap((reference) => getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) continue;
32278
32360
  context.report({
32279
32361
  node: fact.callExpression,
32280
32362
  message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
@@ -34287,7 +34369,7 @@ const declarationBodyContainsHookCall = (symbol) => {
34287
34369
  walkAst(componentFunction, (descendant) => {
34288
34370
  if (didFindHookCall) return false;
34289
34371
  if (!isNodeOfType(descendant, "CallExpression")) return;
34290
- const calleeName = getCalleeName$2(descendant);
34372
+ const calleeName = getCalleeName$1(descendant);
34291
34373
  if (calleeName && isReactHookName(calleeName)) {
34292
34374
  didFindHookCall = true;
34293
34375
  return false;
@@ -34312,7 +34394,7 @@ const isReturnedFromUseCallbackAdapter = (callNode) => {
34312
34394
  if (isNodeOfType(parent, "ArrowFunctionExpression")) {
34313
34395
  if (parent.body !== current) return false;
34314
34396
  const grandparent = parent.parent;
34315
- return isNodeOfType(grandparent, "CallExpression") && getCalleeName$2(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
34397
+ return isNodeOfType(grandparent, "CallExpression") && getCalleeName$1(grandparent) === "useCallback" && grandparent.arguments.some((argumentNode) => argumentNode === parent);
34316
34398
  }
34317
34399
  if (!isNodeOfType(parent, "ConditionalExpression") && !isNodeOfType(parent, "LogicalExpression")) return false;
34318
34400
  current = parent;
@@ -36844,12 +36926,7 @@ const noDerivedState = defineRule({
36844
36926
  for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
36845
36927
  } }).visitors,
36846
36928
  CallExpression(node) {
36847
- if (!isReactApiCall(node, "useEffect", context.scopes, {
36848
- allowGlobalReactNamespace: true,
36849
- allowUnboundBareCalls: true,
36850
- resolveConditionalAliases: true,
36851
- resolveNamedAliases: true
36852
- })) return;
36929
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
36853
36930
  const analysis = getProgramAnalysis(node);
36854
36931
  if (!analysis) return;
36855
36932
  for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
@@ -36869,12 +36946,7 @@ const noDerivedStateEffect = defineRule({
36869
36946
  tags: ["test-noise"],
36870
36947
  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",
36871
36948
  create: (context) => ({ CallExpression(node) {
36872
- if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
36873
- allowGlobalReactNamespace: true,
36874
- allowUnboundBareCalls: true,
36875
- resolveConditionalAliases: true,
36876
- resolveNamedAliases: true
36877
- })) return;
36949
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes)) return;
36878
36950
  const analysis = getProgramAnalysis(node);
36879
36951
  if (!analysis) return;
36880
36952
  if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
@@ -36997,7 +37069,7 @@ const getEnclosingEffectHookCallback = (node, componentFunction) => {
36997
37069
  if (isFunctionLike$1(cursor)) {
36998
37070
  const parent = cursor.parent ?? null;
36999
37071
  if (parent && isNodeOfType(parent, "CallExpression")) {
37000
- const calleeName = getCalleeName$2(parent);
37072
+ const calleeName = getCalleeName$1(parent);
37001
37073
  if (calleeName !== null && EFFECT_HOOK_NAME_PATTERN.test(calleeName) && (parent.arguments ?? []).some((argument) => argument === cursor)) return cursor;
37002
37074
  }
37003
37075
  }
@@ -37068,7 +37140,7 @@ const isNonHandlerHookCallback = (functionNode) => {
37068
37140
  const parent = functionNode.parent ?? null;
37069
37141
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
37070
37142
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
37071
- const calleeName = getCalleeName$2(parent);
37143
+ const calleeName = getCalleeName$1(parent);
37072
37144
  return calleeName !== null && isReactHookName(calleeName) && calleeName !== "useCallback";
37073
37145
  };
37074
37146
  const isHandlerShapedReseed = (setterCall, componentFunction) => {
@@ -37150,7 +37222,7 @@ const noDerivedUseState = defineRule({
37150
37222
  return {
37151
37223
  ...propStackTracker.visitors,
37152
37224
  CallExpression(node) {
37153
- if (!isHookCall$2(node, "useState") || !node.arguments?.length) return;
37225
+ if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
37154
37226
  const seed = unwrapInitializerSeed(node.arguments[0]);
37155
37227
  const reportStalePropCopy = (propName) => {
37156
37228
  if (isIntentionalSnapshotState(node)) return;
@@ -37839,12 +37911,7 @@ const collectUseStateBindings = (componentBody, scopes) => {
37839
37911
  const setterElement = elements[1];
37840
37912
  if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
37841
37913
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
37842
- if (!(scopes ? isReactApiCall(declarator.init, "useState", scopes, {
37843
- allowGlobalReactNamespace: true,
37844
- allowUnboundBareCalls: true,
37845
- resolveConditionalAliases: true,
37846
- resolveNamedAliases: true
37847
- }) : isHookCall$2(declarator.init, "useState"))) continue;
37914
+ if (!isReactHookCall(declarator.init, "useState", scopes)) continue;
37848
37915
  bindings.push({
37849
37916
  valueName: valueElement.name,
37850
37917
  setterName: setterElement.name,
@@ -38011,7 +38078,7 @@ const noDirectStateMutation = defineRule({
38011
38078
  create: (context) => {
38012
38079
  const checkComponent = (componentBody) => {
38013
38080
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38014
- const bindings = collectUseStateBindings(componentBody);
38081
+ const bindings = collectUseStateBindings(componentBody, context.scopes);
38015
38082
  if (bindings.length === 0) return;
38016
38083
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
38017
38084
  const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
@@ -38578,12 +38645,7 @@ const findTopLevelEffectCalls = (componentBody, scopes) => {
38578
38645
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
38579
38646
  const expression = unwrapDiscardedExpression(statement);
38580
38647
  if (!isNodeOfType(expression, "CallExpression")) continue;
38581
- if (!isReactApiCall(expression, EFFECT_HOOK_NAMES$1, scopes, {
38582
- allowGlobalReactNamespace: true,
38583
- allowUnboundBareCalls: true,
38584
- resolveConditionalAliases: true,
38585
- resolveNamedAliases: true
38586
- })) continue;
38648
+ if (!isReactHookCall(expression, EFFECT_HOOK_NAMES$1, scopes)) continue;
38587
38649
  effectCalls.push(expression);
38588
38650
  }
38589
38651
  return effectCalls;
@@ -38827,7 +38889,7 @@ const collectStorageHookSetterNames = (componentBody) => {
38827
38889
  for (const declarator of statement.declarations ?? []) {
38828
38890
  if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
38829
38891
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
38830
- const calleeName = getCalleeName$2(declarator.init);
38892
+ const calleeName = getCalleeName$1(declarator.init);
38831
38893
  if (!calleeName || !STORAGE_HOOK_PATTERN.test(calleeName)) continue;
38832
38894
  for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier") && isSetterIdentifier(element.name)) setterNames.add(element.name);
38833
38895
  }
@@ -39315,7 +39377,7 @@ const noEffectEventHandler = defineRule({
39315
39377
  return {
39316
39378
  ...propStackTracker.visitors,
39317
39379
  CallExpression(node) {
39318
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
39380
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
39319
39381
  const callback = getEffectCallback(node);
39320
39382
  if (!callback) return;
39321
39383
  const analysis = getProgramAnalysis(node);
@@ -39435,14 +39497,14 @@ const noEffectEventInDeps = defineRule({
39435
39497
  if (!isNodeOfType(declaratorNode.id, "Identifier")) return;
39436
39498
  const initializer = declaratorNode.init;
39437
39499
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
39438
- if (!isHookCall$2(initializer, "useEffectEvent")) return;
39500
+ if (!isReactHookCall(initializer, "useEffectEvent", context.scopes)) return;
39439
39501
  if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
39440
39502
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
39441
39503
  } });
39442
39504
  return {
39443
39505
  ...componentBindings.visitors,
39444
39506
  CallExpression(node) {
39445
- if (!isHookCall$2(node, HOOKS_WITH_DEPS) || node.arguments.length < 2) return;
39507
+ if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
39446
39508
  if (!componentBindings.isInsideComponent()) return;
39447
39509
  const depsNode = node.arguments[1];
39448
39510
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
@@ -39499,7 +39561,7 @@ const noEffectWithFreshDeps = defineRule({
39499
39561
  node: finding.reportNode,
39500
39562
  message: `A dependency inside this custom Hook changes every render because \`${finding.bindingName}\` is a new ${finding.kind} built fresh each time.`
39501
39563
  });
39502
- if (!isHookCall$2(node, HOOKS_WITH_DEPS)) return;
39564
+ if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes)) return;
39503
39565
  const args = node.arguments ?? [];
39504
39566
  if (args.length < 2) return;
39505
39567
  const depsNode = args[1];
@@ -39760,7 +39822,7 @@ const noEventHandler = defineRule({
39760
39822
  severity: "warn",
39761
39823
  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",
39762
39824
  create: (context) => ({ CallExpression(node) {
39763
- if (!isUseEffect(node)) return;
39825
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
39764
39826
  const analysis = getProgramAnalysis(node);
39765
39827
  if (!analysis || hasCleanup(analysis, node)) return;
39766
39828
  const frames = collectBoundedEffectExecutionFrames(analysis, node);
@@ -40222,25 +40284,25 @@ const addDeclarationBindings = (statement, scope) => {
40222
40284
  }
40223
40285
  if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) addPatternBindings(statement.id, scope);
40224
40286
  };
40225
- const collectRenderReachableNamesFromStatements = (statements, names, scope, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
40287
+ const collectRenderReachableNamesFromStatements = (statements, names, scope, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
40226
40288
  let hasReturn = false;
40227
- for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, eventHandlerReferenceNames)) hasReturn = true;
40289
+ for (const statement of statements ?? []) if (collectRenderReachableNamesFromStatement(statement, names, scope, scopes, eventHandlerReferenceNames)) hasReturn = true;
40228
40290
  else addDeclarationBindings(statement, scope);
40229
40291
  return hasReturn;
40230
40292
  };
40231
- const collectRenderReachableNamesFromStatement = (statement, names, scope, eventHandlerReferenceNames) => {
40293
+ const collectRenderReachableNamesFromStatement = (statement, names, scope, scopes, eventHandlerReferenceNames) => {
40232
40294
  if (isNodeOfType(statement, "ReturnStatement")) {
40233
40295
  if (statement.argument) addNames(names, collectScopedReferenceNames(statement.argument, scope, eventHandlerReferenceNames));
40234
40296
  return true;
40235
40297
  }
40236
- 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)) {
40298
+ 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)) {
40237
40299
  for (const argument of statement.expression.arguments ?? []) addNames(names, collectScopedReferenceNames(argument, scope, eventHandlerReferenceNames));
40238
40300
  return false;
40239
40301
  }
40240
- if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), eventHandlerReferenceNames);
40302
+ if (isNodeOfType(statement, "BlockStatement")) return collectRenderReachableNamesFromStatements(statement.body, names, createBlockBindingScope(scope), scopes, eventHandlerReferenceNames);
40241
40303
  if (isNodeOfType(statement, "IfStatement")) {
40242
- const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, eventHandlerReferenceNames);
40243
- const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, eventHandlerReferenceNames) : false;
40304
+ const consequentHasReturn = collectRenderReachableNamesFromStatement(statement.consequent, names, scope, scopes, eventHandlerReferenceNames);
40305
+ const alternateHasReturn = statement.alternate ? collectRenderReachableNamesFromStatement(statement.alternate, names, scope, scopes, eventHandlerReferenceNames) : false;
40244
40306
  if (consequentHasReturn || alternateHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
40245
40307
  return consequentHasReturn || alternateHasReturn;
40246
40308
  }
@@ -40248,7 +40310,7 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
40248
40310
  let hasReturn = false;
40249
40311
  for (const switchCase of statement.cases ?? []) {
40250
40312
  const caseScope = createBlockBindingScope(scope);
40251
- if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, eventHandlerReferenceNames)) continue;
40313
+ if (!collectRenderReachableNamesFromStatements(switchCase.consequent, names, caseScope, scopes, eventHandlerReferenceNames)) continue;
40252
40314
  hasReturn = true;
40253
40315
  if (switchCase.test) addNames(names, collectScopedReferenceNames(switchCase.test, scope, eventHandlerReferenceNames));
40254
40316
  }
@@ -40256,25 +40318,25 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
40256
40318
  return hasReturn;
40257
40319
  }
40258
40320
  if (isNodeOfType(statement, "TryStatement")) {
40259
- const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, eventHandlerReferenceNames);
40260
- const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, eventHandlerReferenceNames) : false;
40261
- const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, eventHandlerReferenceNames) : false;
40321
+ const blockHasReturn = collectRenderReachableNamesFromStatement(statement.block, names, scope, scopes, eventHandlerReferenceNames);
40322
+ const handlerHasReturn = statement.handler ? collectRenderReachableNamesFromStatement(statement.handler, names, scope, scopes, eventHandlerReferenceNames) : false;
40323
+ const finalizerHasReturn = statement.finalizer ? collectRenderReachableNamesFromStatement(statement.finalizer, names, scope, scopes, eventHandlerReferenceNames) : false;
40262
40324
  return blockHasReturn || handlerHasReturn || finalizerHasReturn;
40263
40325
  }
40264
40326
  if (isNodeOfType(statement, "CatchClause")) {
40265
40327
  const catchScope = createBlockBindingScope(scope);
40266
40328
  addPatternBindings(statement.param, catchScope);
40267
- return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, eventHandlerReferenceNames);
40329
+ return collectRenderReachableNamesFromStatement(statement.body, names, catchScope, scopes, eventHandlerReferenceNames);
40268
40330
  }
40269
40331
  if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
40270
- const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
40332
+ const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
40271
40333
  if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.test, scope, eventHandlerReferenceNames));
40272
40334
  return bodyHasReturn;
40273
40335
  }
40274
40336
  if (isNodeOfType(statement, "ForStatement")) {
40275
40337
  const loopScope = createBlockBindingScope(scope);
40276
40338
  if (statement.init) addDeclarationBindings(statement.init, loopScope);
40277
- if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
40339
+ if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
40278
40340
  if (statement.init) addNames(names, collectScopedReferenceNames(statement.init, loopScope, eventHandlerReferenceNames));
40279
40341
  if (statement.test) addNames(names, collectScopedReferenceNames(statement.test, loopScope, eventHandlerReferenceNames));
40280
40342
  if (statement.update) addNames(names, collectScopedReferenceNames(statement.update, loopScope, eventHandlerReferenceNames));
@@ -40284,22 +40346,22 @@ const collectRenderReachableNamesFromStatement = (statement, names, scope, event
40284
40346
  const rightNames = collectScopedReferenceNames(statement.right, scope, eventHandlerReferenceNames);
40285
40347
  const loopScope = createBlockBindingScope(scope);
40286
40348
  if (isNodeOfType(statement.left, "VariableDeclaration")) addDeclarationBindings(statement.left, loopScope);
40287
- if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, eventHandlerReferenceNames)) return false;
40349
+ if (!collectRenderReachableNamesFromStatement(statement.body, names, loopScope, scopes, eventHandlerReferenceNames)) return false;
40288
40350
  addNames(names, rightNames);
40289
40351
  return true;
40290
40352
  }
40291
- if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
40353
+ if (isNodeOfType(statement, "LabeledStatement")) return collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
40292
40354
  if (isNodeOfType(statement, "WithStatement")) {
40293
- const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, eventHandlerReferenceNames);
40355
+ const bodyHasReturn = collectRenderReachableNamesFromStatement(statement.body, names, scope, scopes, eventHandlerReferenceNames);
40294
40356
  if (bodyHasReturn) addNames(names, collectScopedReferenceNames(statement.object, scope, eventHandlerReferenceNames));
40295
40357
  return bodyHasReturn;
40296
40358
  }
40297
40359
  return false;
40298
40360
  };
40299
- const collectRenderReachableNames = (componentBody, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
40361
+ const collectRenderReachableNames = (componentBody, scopes, eventHandlerReferenceNames = /* @__PURE__ */ new Set()) => {
40300
40362
  const names = /* @__PURE__ */ new Set();
40301
40363
  if (!isNodeOfType(componentBody, "BlockStatement")) return names;
40302
- collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), eventHandlerReferenceNames);
40364
+ collectRenderReachableNamesFromStatements(componentBody.body, names, createComponentBindingScope(), scopes, eventHandlerReferenceNames);
40303
40365
  return names;
40304
40366
  };
40305
40367
  //#endregion
@@ -40322,42 +40384,36 @@ const expandTransitiveDependencies = (seedNames, dependencyGraph) => {
40322
40384
  };
40323
40385
  //#endregion
40324
40386
  //#region src/plugin/rules/state-and-effects/utils/collect-function-like-local-names.ts
40325
- const isUseCallbackCall = (node) => isNodeOfType(node, "CallExpression") && getCalleeName(node.callee) === "useCallback";
40326
- const getCalleeName = (node) => {
40327
- if (isNodeOfType(node, "Identifier")) return node.name;
40328
- if (isNodeOfType(node, "MemberExpression")) return getStaticMemberPropertyName(node);
40329
- return null;
40330
- };
40331
- const isFunctionLikeReference = (node, functionLikeLocalNames, scope) => {
40332
- if (isInlineFunctionExpression(node) || isUseCallbackCall(node)) return true;
40387
+ const isFunctionLikeReference = (node, functionLikeLocalNames, scope, scopes) => {
40388
+ if (isInlineFunctionExpression(node) || isReactHookCall(node, "useCallback", scopes)) return true;
40333
40389
  if (isNodeOfType(node, "Identifier")) return functionLikeLocalNames.has(resolveBindingName(scope, node.name));
40334
40390
  const memberReferenceName = getStaticMemberReferenceName(node, (name) => resolveBindingName(scope, name));
40335
40391
  return Boolean(memberReferenceName && functionLikeLocalNames.has(memberReferenceName));
40336
40392
  };
40337
- const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope) => {
40393
+ const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLocalNames, scope, scopes) => {
40338
40394
  if (!isNodeOfType(node, "ObjectExpression")) return;
40339
40395
  for (const property of node.properties ?? []) {
40340
40396
  if (!isNodeOfType(property, "Property")) continue;
40341
40397
  const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
40342
40398
  if (!propertyName) continue;
40343
- if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
40399
+ if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope, scopes)) continue;
40344
40400
  functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
40345
40401
  }
40346
40402
  };
40347
- const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope) => {
40403
+ const addVariableDeclarationFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
40348
40404
  if (!isNodeOfType(statement, "VariableDeclaration")) return;
40349
40405
  const declarationScope = getVariableDeclarationScope(statement, scope);
40350
40406
  for (const declarator of statement.declarations ?? []) {
40351
40407
  const declaredBindingNames = addPatternBindings(declarator.id, declarationScope);
40352
40408
  if (!declarator.init) continue;
40353
- const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope);
40409
+ const isFunctionReference = isFunctionLikeReference(declarator.init, functionLikeLocalNames, scope, scopes);
40354
40410
  for (const declaredBindingName of declaredBindingNames) {
40355
40411
  if (isFunctionReference) functionLikeLocalNames.add(declaredBindingName);
40356
- addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope);
40412
+ addObjectPropertyFunctionNames(declaredBindingName, declarator.init, functionLikeLocalNames, scope, scopes);
40357
40413
  }
40358
40414
  }
40359
40415
  };
40360
- const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope) => {
40416
+ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope, scopes) => {
40361
40417
  if (isNodeOfType(statement, "FunctionDeclaration")) {
40362
40418
  if (statement.id) {
40363
40419
  const declaredBindingNames = addPatternBindings(statement.id, scope);
@@ -40366,61 +40422,61 @@ const collectStatementFunctionNames = (statement, functionLikeLocalNames, scope)
40366
40422
  return;
40367
40423
  }
40368
40424
  if (isNodeOfType(statement, "VariableDeclaration")) {
40369
- addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope);
40425
+ addVariableDeclarationFunctionNames(statement, functionLikeLocalNames, scope, scopes);
40370
40426
  return;
40371
40427
  }
40372
40428
  if (isNodeOfType(statement, "BlockStatement")) {
40373
- collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope));
40429
+ collectStatementListFunctionNames(statement.body, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
40374
40430
  return;
40375
40431
  }
40376
40432
  if (isNodeOfType(statement, "IfStatement")) {
40377
- collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope);
40378
- if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope);
40433
+ collectStatementFunctionNames(statement.consequent, functionLikeLocalNames, scope, scopes);
40434
+ if (statement.alternate) collectStatementFunctionNames(statement.alternate, functionLikeLocalNames, scope, scopes);
40379
40435
  return;
40380
40436
  }
40381
40437
  if (isNodeOfType(statement, "SwitchStatement")) {
40382
- for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope));
40438
+ for (const switchCase of statement.cases ?? []) collectStatementListFunctionNames(switchCase.consequent, functionLikeLocalNames, createBlockBindingScope(scope), scopes);
40383
40439
  return;
40384
40440
  }
40385
40441
  if (isNodeOfType(statement, "TryStatement")) {
40386
- collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope);
40442
+ collectStatementFunctionNames(statement.block, functionLikeLocalNames, scope, scopes);
40387
40443
  if (statement.handler) {
40388
40444
  const catchScope = createBlockBindingScope(scope);
40389
40445
  addPatternBindings(statement.handler.param, catchScope);
40390
- collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope);
40446
+ collectStatementFunctionNames(statement.handler.body, functionLikeLocalNames, catchScope, scopes);
40391
40447
  }
40392
- if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope);
40448
+ if (statement.finalizer) collectStatementFunctionNames(statement.finalizer, functionLikeLocalNames, scope, scopes);
40393
40449
  return;
40394
40450
  }
40395
40451
  if (isNodeOfType(statement, "ForStatement")) {
40396
40452
  const loopScope = createBlockBindingScope(scope);
40397
- if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope);
40398
- collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
40453
+ if (statement.init && isNodeOfType(statement.init, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.init, functionLikeLocalNames, loopScope, scopes);
40454
+ collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
40399
40455
  return;
40400
40456
  }
40401
40457
  if (isNodeOfType(statement, "ForInStatement") || isNodeOfType(statement, "ForOfStatement")) {
40402
40458
  const loopScope = createBlockBindingScope(scope);
40403
- if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope);
40459
+ if (isNodeOfType(statement.left, "VariableDeclaration")) addVariableDeclarationFunctionNames(statement.left, functionLikeLocalNames, loopScope, scopes);
40404
40460
  else addPatternBindings(statement.left, loopScope);
40405
- collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope);
40461
+ collectStatementFunctionNames(statement.body, functionLikeLocalNames, loopScope, scopes);
40406
40462
  return;
40407
40463
  }
40408
40464
  if (isNodeOfType(statement, "WhileStatement") || isNodeOfType(statement, "DoWhileStatement")) {
40409
- collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
40465
+ collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
40410
40466
  return;
40411
40467
  }
40412
- if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope);
40468
+ if (isNodeOfType(statement, "LabeledStatement")) collectStatementFunctionNames(statement.body, functionLikeLocalNames, scope, scopes);
40413
40469
  };
40414
- const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope) => {
40415
- for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope);
40470
+ const collectStatementListFunctionNames = (statements, functionLikeLocalNames, scope, scopes) => {
40471
+ for (const statement of statements ?? []) collectStatementFunctionNames(statement, functionLikeLocalNames, scope, scopes);
40416
40472
  };
40417
- const collectFunctionLikeLocalNames = (componentBody) => {
40473
+ const collectFunctionLikeLocalNames = (componentBody, scopes) => {
40418
40474
  const functionLikeLocalNames = /* @__PURE__ */ new Set();
40419
40475
  if (!isNodeOfType(componentBody, "BlockStatement")) return functionLikeLocalNames;
40420
40476
  let previousSize = -1;
40421
40477
  while (previousSize !== functionLikeLocalNames.size) {
40422
40478
  previousSize = functionLikeLocalNames.size;
40423
- collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope());
40479
+ collectStatementListFunctionNames(componentBody.body, functionLikeLocalNames, createComponentBindingScope(), scopes);
40424
40480
  }
40425
40481
  return functionLikeLocalNames;
40426
40482
  };
@@ -40460,17 +40516,17 @@ const noEventTriggerState = defineRule({
40460
40516
  create: (context) => {
40461
40517
  const checkComponent = (componentBody) => {
40462
40518
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
40463
- const useStateBindings = collectUseStateBindings(componentBody);
40519
+ const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
40464
40520
  if (useStateBindings.length === 0) return;
40465
40521
  const analysis = getProgramAnalysis(componentBody);
40466
40522
  if (!analysis) return;
40467
40523
  const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
40468
- const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
40524
+ const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
40469
40525
  const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
40470
- const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
40526
+ const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames), dependencyGraph);
40471
40527
  walkAst(componentBody, (effectCall) => {
40472
40528
  if (!isNodeOfType(effectCall, "CallExpression")) return;
40473
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) return;
40529
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) return;
40474
40530
  if ((effectCall.arguments?.length ?? 0) < 2) return;
40475
40531
  const depsNode = effectCall.arguments[1];
40476
40532
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
@@ -42299,7 +42355,7 @@ const noInitializeState = defineRule({
42299
42355
  tags: ["test-noise"],
42300
42356
  recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
42301
42357
  create: (context) => ({ CallExpression(node) {
42302
- if (!isUseEffect(node)) return;
42358
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
42303
42359
  const dependencies = node.arguments?.[1];
42304
42360
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
42305
42361
  const analysis = getProgramAnalysis(node);
@@ -43911,7 +43967,7 @@ const noMirrorPropEffect = defineRule({
43911
43967
  const setterElement = elements[1];
43912
43968
  if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
43913
43969
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
43914
- if (!isHookCall$2(declarator.init, "useState")) continue;
43970
+ if (!isReactHookCall(declarator.init, "useState", context.scopes)) continue;
43915
43971
  const initializer = declarator.init.arguments?.[0];
43916
43972
  if (!initializer) continue;
43917
43973
  const propRootName = getPropRootName(initializer, propNames);
@@ -43929,7 +43985,7 @@ const noMirrorPropEffect = defineRule({
43929
43985
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
43930
43986
  const effectCall = unwrapDiscardedExpression(statement);
43931
43987
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
43932
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
43988
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
43933
43989
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
43934
43990
  const depsNode = effectCall.arguments[1];
43935
43991
  if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
@@ -44338,7 +44394,7 @@ const noMultiComp = defineRule({
44338
44394
  });
44339
44395
  //#endregion
44340
44396
  //#region src/plugin/rules/state-and-effects/no-mutable-in-deps.ts
44341
- const collectUseRefBindingNames = (componentBody) => {
44397
+ const collectUseRefBindingNames = (componentBody, scopes) => {
44342
44398
  const useRefBindings = /* @__PURE__ */ new Set();
44343
44399
  if (!isNodeOfType(componentBody, "BlockStatement")) return useRefBindings;
44344
44400
  for (const statement of componentBody.body ?? []) {
@@ -44346,7 +44402,7 @@ const collectUseRefBindingNames = (componentBody) => {
44346
44402
  for (const declarator of statement.declarations ?? []) {
44347
44403
  if (!isNodeOfType(declarator.id, "Identifier")) continue;
44348
44404
  if (!isNodeOfType(declarator.init, "CallExpression")) continue;
44349
- if (!isHookCall$2(declarator.init, "useRef")) continue;
44405
+ if (!isReactHookCall(declarator.init, "useRef", scopes)) continue;
44350
44406
  useRefBindings.add(declarator.id.name);
44351
44407
  }
44352
44408
  }
@@ -44381,12 +44437,12 @@ const noMutableInDeps = defineRule({
44381
44437
  create: (context) => {
44382
44438
  const checkComponent = (componentBody, componentParams = []) => {
44383
44439
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
44384
- const useRefBindingNames = collectUseRefBindingNames(componentBody);
44440
+ const useRefBindingNames = collectUseRefBindingNames(componentBody, context.scopes);
44385
44441
  const localBindingNames = collectLocalBindingNames(componentBody);
44386
44442
  for (const param of componentParams) collectPatternNames(param, localBindingNames);
44387
44443
  walkAst(componentBody, (child) => {
44388
44444
  if (!isNodeOfType(child, "CallExpression")) return;
44389
- if (!isHookCall$2(child, HOOKS_WITH_DEPS)) return;
44445
+ if (!isReactHookCall(child, HOOKS_WITH_DEPS, context.scopes)) return;
44390
44446
  if ((child.arguments?.length ?? 0) < 2) return;
44391
44447
  const depsNode = child.arguments[1];
44392
44448
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
@@ -46299,16 +46355,10 @@ const noPassDataToParent = defineRule({
46299
46355
  tags: ["test-noise"],
46300
46356
  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",
46301
46357
  create: (context) => {
46302
- const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
46303
- allowGlobalReactNamespace: true,
46304
- allowUnboundBareCalls: true
46305
- });
46306
- const isReactUseEffectCall = (node) => isReactApiCall(node, "useEffect", context.scopes, {
46307
- allowGlobalReactNamespace: true,
46308
- allowUnboundBareCalls: true
46309
- });
46358
+ const isReactUseRefCall = (node) => isReactHookCall(node, "useRef", context.scopes);
46359
+ const isReactUseEffectCall = (node) => isReactHookCall(node, "useEffect", context.scopes);
46310
46360
  return { CallExpression(node) {
46311
- if (!isUseEffect(node)) return;
46361
+ if (!isReactUseEffectCall(node)) return;
46312
46362
  const analysis = getProgramAnalysis(node);
46313
46363
  if (!analysis) return;
46314
46364
  if (hasCleanup(analysis, node)) return;
@@ -46595,7 +46645,7 @@ const noPassLiveStateToParent = defineRule({
46595
46645
  tags: ["test-noise"],
46596
46646
  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",
46597
46647
  create: (context) => ({ CallExpression(node) {
46598
- if (!isUseEffect(node)) return;
46648
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
46599
46649
  const analysis = getProgramAnalysis(node);
46600
46650
  if (!analysis) return;
46601
46651
  const effectFnRefs = getEffectFnRefs(analysis, node);
@@ -47010,7 +47060,7 @@ const hasPreviousValueDep = (effectNode, depElements) => {
47010
47060
  if (!isNodeOfType(element, "Identifier")) continue;
47011
47061
  const binding = findVariableInitializer(effectNode, element.name);
47012
47062
  if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) continue;
47013
- const calleeName = getCalleeName$2(binding.initializer);
47063
+ const calleeName = getCalleeName$1(binding.initializer);
47014
47064
  if (calleeName && PREVIOUS_VALUE_HOOK_PATTERN.test(calleeName)) return true;
47015
47065
  }
47016
47066
  return false;
@@ -47032,7 +47082,7 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
47032
47082
  if (!isNodeOfType(receiver, "Identifier")) return null;
47033
47083
  const binding = findVariableInitializer(callExpression, receiver.name);
47034
47084
  if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
47035
- if (getCalleeName$2(binding.initializer) !== "useRef") return null;
47085
+ if (getCalleeName$1(binding.initializer) !== "useRef") return null;
47036
47086
  const callbackArgument = binding.initializer.arguments?.[0];
47037
47087
  if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
47038
47088
  return isPropName(callbackArgument.name) ? callbackArgument.name : null;
@@ -47064,7 +47114,7 @@ const noPropCallbackInEffect = defineRule({
47064
47114
  return {
47065
47115
  ...propStackTracker.visitors,
47066
47116
  CallExpression(node) {
47067
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
47117
+ if (!isReactHookCall(node, EFFECT_HOOK_NAMES$1, context.scopes) || (node.arguments?.length ?? 0) < 2) return;
47068
47118
  const callback = getEffectCallback(node);
47069
47119
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
47070
47120
  const depsNode = node.arguments[1];
@@ -48957,7 +49007,7 @@ const noResetAllStateOnPropChange = defineRule({
48957
49007
  tags: ["test-noise"],
48958
49008
  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",
48959
49009
  create: (context) => ({ CallExpression(node) {
48960
- if (!isUseEffect(node)) return;
49010
+ if (!isReactHookCall(node, "useEffect", context.scopes)) return;
48961
49011
  const analysis = getProgramAnalysis(node);
48962
49012
  if (!analysis) return;
48963
49013
  const effectFnRefs = getEffectFnRefs(analysis, node);
@@ -49226,7 +49276,7 @@ const isTanStackServerFnHandlerCall = (node) => {
49226
49276
  if (node.callee.property.name !== "handler") return false;
49227
49277
  let currentNode = node.callee.object;
49228
49278
  while (isNodeOfType(currentNode, "CallExpression")) {
49229
- const calleeName = getCalleeName$2(currentNode);
49279
+ const calleeName = getCalleeName$1(currentNode);
49230
49280
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) return true;
49231
49281
  if (!isNodeOfType(currentNode.callee, "MemberExpression")) return false;
49232
49282
  currentNode = currentNode.callee.object;
@@ -49727,7 +49777,7 @@ const noSelfUpdatingEffect = defineRule({
49727
49777
  create: (context) => {
49728
49778
  const checkFunctionScope = (functionBody) => {
49729
49779
  if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
49730
- const useStateBindings = collectUseStateBindings(functionBody);
49780
+ const useStateBindings = collectUseStateBindings(functionBody, context.scopes);
49731
49781
  if (useStateBindings.length === 0) return;
49732
49782
  const setterNameToStateName = /* @__PURE__ */ new Map();
49733
49783
  for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
@@ -49736,7 +49786,7 @@ const noSelfUpdatingEffect = defineRule({
49736
49786
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
49737
49787
  const effectCall = unwrapDiscardedExpression(statement);
49738
49788
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
49739
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
49789
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
49740
49790
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
49741
49791
  const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
49742
49792
  if (dependencyStateNames.size === 0) continue;
@@ -49822,7 +49872,7 @@ const noSetStateInRender = defineRule({
49822
49872
  create: (context) => {
49823
49873
  const checkComponent = (componentBody) => {
49824
49874
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
49825
- const setterNames = new Set(collectUseStateBindings(componentBody).map((binding) => binding.setterName));
49875
+ const setterNames = new Set(collectUseStateBindings(componentBody, context.scopes).map((binding) => binding.setterName));
49826
49876
  if (setterNames.size === 0) return;
49827
49877
  for (const statement of componentBody.body ?? []) {
49828
49878
  const setterCall = isUnconditionalSetterCallStatement(statement, setterNames);
@@ -50071,10 +50121,10 @@ const collectTimerRefUsageFacts = (ownerScope, refName) => {
50071
50121
  });
50072
50122
  return facts;
50073
50123
  };
50074
- const isEffectCallbackFunction = (functionNode) => {
50124
+ const isEffectCallbackFunction = (functionNode, scopes) => {
50075
50125
  const parent = functionNode.parent;
50076
50126
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
50077
- return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
50127
+ return isReactHookCall(parent, EFFECT_HOOK_NAMES$1, scopes) && getEffectCallback(parent) === functionNode;
50078
50128
  };
50079
50129
  const doesEffectCallbackReturnName = (effectCallback, name) => {
50080
50130
  if (!isFunctionLike$1(effectCallback)) return false;
@@ -50093,24 +50143,24 @@ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
50093
50143
  const cleanupBindingName = getFunctionBindingName$1(functionNode);
50094
50144
  return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
50095
50145
  };
50096
- const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
50146
+ const isReturnedFromAnyEffectInScope = (functionNode, ownerScope, scopes) => {
50097
50147
  const cleanupBindingName = getFunctionBindingName$1(functionNode);
50098
50148
  if (cleanupBindingName === null) return false;
50099
50149
  let isReturnedFromEffect = false;
50100
50150
  walkAst(ownerScope, (child) => {
50101
50151
  if (isReturnedFromEffect) return false;
50102
- if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
50152
+ if (!isNodeOfType(child, "CallExpression") || !isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
50103
50153
  const effectCallback = getEffectCallback(child);
50104
50154
  if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
50105
50155
  });
50106
50156
  return isReturnedFromEffect;
50107
50157
  };
50108
- const isInsideEffectCleanupReturn = (node, ownerScope) => {
50158
+ const isInsideEffectCleanupReturn = (node, ownerScope, scopes) => {
50109
50159
  let functionNode = findEnclosingFunction$1(node);
50110
50160
  while (functionNode) {
50111
50161
  const outerFunction = findEnclosingFunction$1(functionNode);
50112
- if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
50113
- if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
50162
+ if (outerFunction && isEffectCallbackFunction(outerFunction, scopes) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
50163
+ if (isReturnedFromAnyEffectInScope(functionNode, ownerScope, scopes)) return true;
50114
50164
  functionNode = outerFunction;
50115
50165
  }
50116
50166
  return false;
@@ -50146,10 +50196,10 @@ const noStaleTimerRef = defineRule({
50146
50196
  if (isShadowedTimerGlobal(node)) return;
50147
50197
  const { clearCalleeName, refName } = clearCall;
50148
50198
  const refBinding = findVariableInitializer(node, refName);
50149
- if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
50199
+ if (!refBinding?.initializer || !isReactHookCall(refBinding.initializer, "useRef", context.scopes)) return;
50150
50200
  const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
50151
50201
  if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
50152
- if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
50202
+ if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner, context.scopes)) return;
50153
50203
  if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
50154
50204
  context.report({
50155
50205
  node,
@@ -55953,7 +56003,7 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
55953
56003
  allReadsAreInSubHandlers = false;
55954
56004
  return;
55955
56005
  }
55956
- if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$2(subHandlerCall);
56006
+ if (firstSubHandlerName === null) firstSubHandlerName = getCalleeName$1(subHandlerCall);
55957
56007
  });
55958
56008
  return {
55959
56009
  hasAnyRead,
@@ -55976,7 +56026,7 @@ const preferUseEffectEvent = defineRule({
55976
56026
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
55977
56027
  const effectCall = statement.expression;
55978
56028
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
55979
- if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
56029
+ if (!isReactHookCall(effectCall, EFFECT_HOOK_NAMES$1, context.scopes)) continue;
55980
56030
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
55981
56031
  const depsNode = effectCall.arguments[1];
55982
56032
  if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
@@ -56008,11 +56058,11 @@ const preferUseEffectEvent = defineRule({
56008
56058
  });
56009
56059
  //#endregion
56010
56060
  //#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
56011
- const findUseEffectsInComponent = (componentBody) => {
56061
+ const findUseEffectsInComponent = (componentBody, scopes) => {
56012
56062
  const effectCalls = [];
56013
56063
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
56014
56064
  for (const statement of componentBody.body ?? []) walkAst(statement, (child) => {
56015
- if (isNodeOfType(child, "CallExpression") && isHookCall$2(child, EFFECT_HOOK_NAMES$1)) effectCalls.push(child);
56065
+ if (isNodeOfType(child, "CallExpression") && isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) effectCalls.push(child);
56016
56066
  });
56017
56067
  return effectCalls;
56018
56068
  };
@@ -56215,7 +56265,7 @@ const preferUseSyncExternalStore = defineRule({
56215
56265
  };
56216
56266
  const checkComponent = (componentBody) => {
56217
56267
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
56218
- const useStateBindings = collectUseStateBindings(componentBody);
56268
+ const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
56219
56269
  if (useStateBindings.length === 0) return;
56220
56270
  const useStateInitializerByValueName = /* @__PURE__ */ new Map();
56221
56271
  for (const binding of useStateBindings) {
@@ -56228,7 +56278,7 @@ const preferUseSyncExternalStore = defineRule({
56228
56278
  }
56229
56279
  const setterNameToValueName = /* @__PURE__ */ new Map();
56230
56280
  for (const binding of useStateBindings) setterNameToValueName.set(binding.setterName, binding.valueName);
56231
- for (const effectCall of findUseEffectsInComponent(componentBody)) {
56281
+ for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
56232
56282
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
56233
56283
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
56234
56284
  const depsNode = effectCall.arguments[1];
@@ -56270,7 +56320,7 @@ const preferUseSyncExternalStore = defineRule({
56270
56320
  })).filter((candidate) => candidate.storeName !== null);
56271
56321
  if (snapshotBindings.length === 0) return;
56272
56322
  const reportedDeclarators = /* @__PURE__ */ new Set();
56273
- for (const effectCall of findUseEffectsInComponent(componentBody)) {
56323
+ for (const effectCall of findUseEffectsInComponent(componentBody, context.scopes)) {
56274
56324
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
56275
56325
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
56276
56326
  const depsNode = effectCall.arguments[1];
@@ -56367,7 +56417,7 @@ const preferUseReducer = defineRule({
56367
56417
  create: (context) => {
56368
56418
  const reportCoUpdatedUseState = (body, componentName) => {
56369
56419
  if (!isNodeOfType(body, "BlockStatement")) return;
56370
- const bindings = collectUseStateBindings(body);
56420
+ const bindings = collectUseStateBindings(body, context.scopes);
56371
56421
  const setterNames = new Set(bindings.map((binding) => binding.setterName));
56372
56422
  if (setterNames.size < 5) return;
56373
56423
  const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames, new Map(bindings.map((binding) => {
@@ -56583,7 +56633,7 @@ const QUERY_READ_METHOD_NAMES = new Set([
56583
56633
  ]);
56584
56634
  const isQueryCacheSourceCall = (initializer) => {
56585
56635
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
56586
- const hookName = getCalleeName$2(initializer);
56636
+ const hookName = getCalleeName$1(initializer);
56587
56637
  if (!hookName) return false;
56588
56638
  return hookName === "useQueryClient" || TRPC_UTILS_HOOK_PATTERN.test(hookName);
56589
56639
  };
@@ -56715,7 +56765,7 @@ const queryMutationMissingInvalidation = defineRule({
56715
56765
  },
56716
56766
  CallExpression(node) {
56717
56767
  if (!hasQueryReadUsage) {
56718
- const callName = getCalleeName$2(node);
56768
+ const callName = getCalleeName$1(node);
56719
56769
  if (callName && (QUERY_READ_HOOK_NAMES.has(callName) || QUERY_READ_METHOD_NAMES.has(callName) || TRPC_UTILS_HOOK_PATTERN.test(callName))) hasQueryReadUsage = true;
56720
56770
  }
56721
56771
  const calleeName = isNodeOfType(node.callee, "Identifier") ? node.callee.name : null;
@@ -58378,6 +58428,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
58378
58428
  const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
58379
58429
  const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
58380
58430
  const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
58431
+ const FILE_READER_READ_METHOD_NAMES = new Set([
58432
+ "readAsArrayBuffer",
58433
+ "readAsBinaryString",
58434
+ "readAsDataURL",
58435
+ "readAsText"
58436
+ ]);
58437
+ const isGlobalFileReaderConstruction = (expression, context) => {
58438
+ if (!expression) return false;
58439
+ const unwrappedExpression = stripParenExpression(expression);
58440
+ if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
58441
+ return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
58442
+ };
58443
+ const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
58444
+ const readFunction = findEnclosingFunction$1(readCall);
58445
+ let latestValue = null;
58446
+ let latestStart = null;
58447
+ if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
58448
+ latestValue = readerSymbol.initializer;
58449
+ latestStart = readerSymbol.declarationNode.range[0];
58450
+ }
58451
+ for (const reference of readerSymbol.references) {
58452
+ if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
58453
+ const assignment = reference.identifier.parent;
58454
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
58455
+ latestValue = assignment.right;
58456
+ latestStart = reference.identifier.range[0];
58457
+ }
58458
+ return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
58459
+ };
58460
+ const resolveLoadingCompletionFunction = (expression, context) => {
58461
+ const directFunction = resolveExactLocalFunction(expression, context.scopes);
58462
+ if (directFunction) return directFunction;
58463
+ const unwrappedExpression = stripParenExpression(expression);
58464
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
58465
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
58466
+ const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
58467
+ if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
58468
+ const callback = initializer.arguments?.[0];
58469
+ return callback && isFunctionLike$1(callback) ? callback : null;
58470
+ };
58471
+ const isSetterBooleanCall = (node, setterSymbol, value, context) => {
58472
+ if (!isNodeOfType(node, "CallExpression")) return false;
58473
+ const callee = stripParenExpression(node.callee);
58474
+ const argument = node.arguments?.[0];
58475
+ const unwrappedArgument = argument ? stripParenExpression(argument) : null;
58476
+ return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
58477
+ };
58478
+ const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
58479
+ if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
58480
+ visitedFunctions.add(functionNode);
58481
+ let didClearLoadingState = false;
58482
+ walkAst(functionNode.body, (child) => {
58483
+ if (didClearLoadingState) return false;
58484
+ if (child !== functionNode.body && isFunctionLike$1(child)) return false;
58485
+ if (!isNodeOfType(child, "CallExpression")) return;
58486
+ if (isSetterBooleanCall(child, setterSymbol, false, context)) {
58487
+ didClearLoadingState = true;
58488
+ return false;
58489
+ }
58490
+ const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
58491
+ if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
58492
+ didClearLoadingState = true;
58493
+ return false;
58494
+ }
58495
+ });
58496
+ return didClearLoadingState;
58497
+ };
58498
+ const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
58499
+ const readFunction = findEnclosingFunction$1(readCall);
58500
+ if (!readFunction || !isFunctionLike$1(readFunction)) return null;
58501
+ let callback = null;
58502
+ let callbackStart = originStart;
58503
+ walkAst(readFunction.body, (child) => {
58504
+ if (child !== readFunction.body && isFunctionLike$1(child)) return false;
58505
+ 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;
58506
+ const receiver = stripParenExpression(child.left.object);
58507
+ if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
58508
+ callback = child.right;
58509
+ callbackStart = child.range[0];
58510
+ });
58511
+ return callback;
58512
+ };
58513
+ const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
58514
+ const readFunction = findEnclosingFunction$1(readCall);
58515
+ if (!readFunction || !isFunctionLike$1(readFunction)) return false;
58516
+ let didStartLoading = false;
58517
+ walkAst(readFunction.body, (child) => {
58518
+ if (didStartLoading) return false;
58519
+ if (child !== readFunction.body && isFunctionLike$1(child)) return false;
58520
+ if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
58521
+ if (isSetterBooleanCall(child, setterSymbol, true, context)) {
58522
+ didStartLoading = true;
58523
+ return false;
58524
+ }
58525
+ });
58526
+ return didStartLoading;
58527
+ };
58528
+ const setterTracksFileReader = (functionBody, setterSymbol, context) => {
58529
+ let didFindFileReaderLifecycle = false;
58530
+ walkAst(functionBody, (child) => {
58531
+ if (didFindFileReaderLifecycle) return false;
58532
+ if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
58533
+ const receiver = stripParenExpression(child.callee.object);
58534
+ if (!isNodeOfType(receiver, "Identifier")) return;
58535
+ const readerSymbol = context.scopes.symbolFor(receiver);
58536
+ if (!readerSymbol) return;
58537
+ const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
58538
+ if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
58539
+ const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
58540
+ const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
58541
+ const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
58542
+ const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
58543
+ if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
58544
+ didFindFileReaderLifecycle = true;
58545
+ return false;
58546
+ }
58547
+ });
58548
+ return didFindFileReaderLifecycle;
58549
+ };
58381
58550
  const hasAsyncLoadingWork = (fnBody, setterName) => {
58382
58551
  let found = false;
58383
58552
  walkAst(fnBody, (child) => {
@@ -58551,6 +58720,8 @@ const renderingUsetransitionLoading = defineRule({
58551
58720
  const fnBody = enclosingFunctionBody(node);
58552
58721
  if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
58553
58722
  if (fnBody && setterName) {
58723
+ const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
58724
+ if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
58554
58725
  if (setterEscapes(fnBody, setterName, node)) return;
58555
58726
  if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
58556
58727
  if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
@@ -58882,7 +59053,7 @@ const rerenderDependencies = defineRule({
58882
59053
  severity: "error",
58883
59054
  recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
58884
59055
  create: (context) => ({ CallExpression(node) {
58885
- if (!isHookCall$2(node, HOOKS_WITH_DEPS) || node.arguments.length < 2) return;
59056
+ if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
58886
59057
  const depsNode = node.arguments[1];
58887
59058
  if (!isNodeOfType(depsNode, "ArrayExpression")) return;
58888
59059
  for (const element of depsNode.elements ?? []) {
@@ -59137,7 +59308,7 @@ const rerenderLazyRefInit = defineRule({
59137
59308
  category: "Performance",
59138
59309
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
59139
59310
  create: (context) => ({ CallExpression(node) {
59140
- if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
59311
+ if (!isReactHookCall(node, "useRef", context.scopes) || !node.arguments?.length) return;
59141
59312
  const initializer = stripParenExpression(node.arguments[0]);
59142
59313
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
59143
59314
  const isNewCall = isNodeOfType(initializer, "NewExpression");
@@ -59201,7 +59372,7 @@ const rerenderLazyStateInit = defineRule({
59201
59372
  category: "Performance",
59202
59373
  recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
59203
59374
  create: (context) => ({ CallExpression(node) {
59204
- if (!isHookCall$2(node, "useState") || !node.arguments?.length) return;
59375
+ if (!isReactHookCall(node, "useState", context.scopes) || !node.arguments?.length) return;
59205
59376
  const initializer = findEagerInitializerCall(node.arguments[0]);
59206
59377
  if (!initializer) return;
59207
59378
  const isConstructor = isNodeOfType(initializer, "NewExpression");
@@ -59964,11 +60135,11 @@ const isInsideConditionTest = (identifier, stopAt) => {
59964
60135
  }
59965
60136
  return false;
59966
60137
  };
59967
- const collectEffectDependencyInfos = (componentBody, setterNames) => {
60138
+ const collectEffectDependencyInfos = (componentBody, setterNames, scopes) => {
59968
60139
  const effectInfos = [];
59969
60140
  walkAst(componentBody, (child) => {
59970
60141
  if (!isNodeOfType(child, "CallExpression")) return;
59971
- if (!isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
60142
+ if (!isReactHookCall(child, EFFECT_HOOK_NAMES$1, scopes)) return;
59972
60143
  const dependencyNames = /* @__PURE__ */ new Set();
59973
60144
  for (const argument of child.arguments ?? []) {
59974
60145
  if (!isNodeOfType(argument, "ArrayExpression")) continue;
@@ -60024,13 +60195,14 @@ const collectEffectDependencyInfos = (componentBody, setterNames) => {
60024
60195
  });
60025
60196
  return effectInfos;
60026
60197
  };
60027
- const collectCustomHookArgumentNames = (componentBody) => {
60198
+ const collectCustomHookArgumentNames = (componentBody, scopes) => {
60028
60199
  const argumentNames = /* @__PURE__ */ new Set();
60029
60200
  walkAst(componentBody, (child) => {
60030
60201
  if (!isNodeOfType(child, "CallExpression")) return;
60031
60202
  if (!isNodeOfType(child.callee, "Identifier")) return;
60032
60203
  const calleeName = child.callee.name;
60033
60204
  if (!isReactHookName(calleeName)) return;
60205
+ if (isReactHookCall(child, BUILTIN_HOOK_NAMES, scopes)) return;
60034
60206
  if (BUILTIN_HOOK_NAMES.has(calleeName)) return;
60035
60207
  if (EFFECT_HOOK_NAMES$1.has(calleeName)) return;
60036
60208
  for (const argument of child.arguments ?? []) walkAst(argument, (argumentNode) => {
@@ -60071,21 +60243,21 @@ const rerenderStateOnlyInHandlers = defineRule({
60071
60243
  create: (context) => {
60072
60244
  const checkComponent = (componentBody) => {
60073
60245
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
60074
- const bindings = collectUseStateBindings(componentBody);
60246
+ const bindings = collectUseStateBindings(componentBody, context.scopes);
60075
60247
  if (bindings.length === 0) return;
60076
60248
  if (collectRenderReachableExpressions(componentBody).length === 0) return;
60077
- const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
60249
+ const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
60078
60250
  const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
60079
- const directRenderNames = collectRenderReachableNames(componentBody, eventHandlerReferenceNames);
60251
+ const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
60080
60252
  if (hasRenderPhaseNonHookCall(componentBody)) for (const voidMarkedName of collectTopLevelVoidMarkedNames(componentBody)) directRenderNames.add(voidMarkedName);
60081
60253
  const renderReachableNames = expandTransitiveDependencies(directRenderNames, dependencyGraph);
60082
60254
  const setterNames = new Set(bindings.map((binding) => binding.setterName));
60083
- const effectInfos = collectEffectDependencyInfos(componentBody, setterNames);
60255
+ const effectInfos = collectEffectDependencyInfos(componentBody, setterNames, context.scopes);
60084
60256
  const selfEchoValueNames = /* @__PURE__ */ new Set();
60085
60257
  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);
60086
60258
  const effectConsumedNames = /* @__PURE__ */ new Set();
60087
60259
  for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
60088
- for (const hookArgumentName of collectCustomHookArgumentNames(componentBody)) effectConsumedNames.add(hookArgumentName);
60260
+ for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
60089
60261
  for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
60090
60262
  const calledSetterNames = /* @__PURE__ */ new Set();
60091
60263
  walkAst(componentBody, (child) => {
@@ -67986,7 +68158,7 @@ const declarationAwaitsGate = (declaration, context) => {
67986
68158
  if (!isNodeOfType(argument, "CallExpression")) continue;
67987
68159
  if (hasPossibleStaticMemberCallWrite(argument, context.scopes)) return true;
67988
68160
  if (getOrderIndependentLocalFunction(argument, context.scopes) !== null) continue;
67989
- const calleeName = getCalleeName$2(argument);
68161
+ const calleeName = getCalleeName$1(argument);
67990
68162
  if (!calleeName) continue;
67991
68163
  if (isAuthGuardName(calleeName)) return true;
67992
68164
  const [leadingToken] = tokenizeIdentifierWords(calleeName);
@@ -68626,7 +68798,7 @@ const walkServerFnChain = (outerNode) => {
68626
68798
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
68627
68799
  let currentNode = stripParenExpression(outerNode.callee.object);
68628
68800
  while (isNodeOfType(currentNode, "CallExpression")) {
68629
- const calleeName = getCalleeName$2(currentNode);
68801
+ const calleeName = getCalleeName$1(currentNode);
68630
68802
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
68631
68803
  result.isServerFnChain = true;
68632
68804
  const optionsArgument = currentNode.arguments?.[0];