oxlint-plugin-react-doctor 0.7.4-dev.b47d053 → 0.7.4-dev.d4f2209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +138 -0
  2. package/dist/index.js +2326 -809
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -553,7 +553,7 @@ const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
553
553
  "ResizeObserver",
554
554
  "PerformanceObserver"
555
555
  ]);
556
- const STORAGE_OBJECTS$1 = new Set(["localStorage", "sessionStorage"]);
556
+ const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
557
557
  //#endregion
558
558
  //#region src/plugin/constants/react.ts
559
559
  const INDEX_PARAMETER_NAMES = new Set([
@@ -5139,8 +5139,10 @@ const STORAGE_GLOBALS = new Set([
5139
5139
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
5140
5140
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
5141
5141
  const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
5142
+ const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
5142
5143
  const isAuthCredentialKey = (key) => {
5143
5144
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
5145
+ if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
5144
5146
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
5145
5147
  return true;
5146
5148
  };
@@ -6135,7 +6137,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
6135
6137
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6136
6138
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6137
6139
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6138
- const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6139
6140
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6140
6141
  const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6141
6142
  const resolveStringKey = (keyArg, context) => {
@@ -6158,7 +6159,7 @@ const clientLocalstorageNoVersion = defineRule({
6158
6159
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6159
6160
  const receiver = stripParenExpression(node.callee.object);
6160
6161
  if (!isNodeOfType(receiver, "Identifier")) return;
6161
- if (!STORAGE_OBJECTS.has(receiver.name)) return;
6162
+ if (receiver.name !== "localStorage") return;
6162
6163
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6163
6164
  if (node.callee.property.name !== "setItem") return;
6164
6165
  const keyArg = node.arguments?.[0];
@@ -8406,6 +8407,54 @@ const effectListenerCleanupMismatch = defineRule({
8406
8407
  } })
8407
8408
  });
8408
8409
  //#endregion
8410
+ //#region src/plugin/utils/collect-effect-invoked-functions.ts
8411
+ const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
8412
+ "then",
8413
+ "catch",
8414
+ "finally"
8415
+ ]);
8416
+ const collectEffectInvokedFunctions = (effectCallback) => {
8417
+ const invokedFunctions = new Set([effectCallback]);
8418
+ const localFunctionBindings = /* @__PURE__ */ new Map();
8419
+ const calledBindingNames = /* @__PURE__ */ new Set();
8420
+ const pendingFunctions = [effectCallback];
8421
+ const enqueue = (candidate) => {
8422
+ const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
8423
+ if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
8424
+ invokedFunctions.add(strippedCandidate);
8425
+ pendingFunctions.push(strippedCandidate);
8426
+ };
8427
+ const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
8428
+ while (pendingFunctions.length > 0) {
8429
+ const currentFunction = pendingFunctions.pop();
8430
+ if (!currentFunction) break;
8431
+ walkAst(currentFunction, (child) => {
8432
+ if (child !== currentFunction && isFunctionLike$1(child)) {
8433
+ if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
8434
+ return false;
8435
+ }
8436
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
8437
+ const initializer = child.init ? stripParenExpression(child.init) : null;
8438
+ if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
8439
+ return;
8440
+ }
8441
+ if (!isNodeOfType(child, "CallExpression")) return;
8442
+ const callee = stripParenExpression(child.callee);
8443
+ if (isFunctionLike$1(callee)) {
8444
+ enqueue(callee);
8445
+ return;
8446
+ }
8447
+ if (isNodeOfType(callee, "Identifier")) {
8448
+ calledBindingNames.add(callee.name);
8449
+ return;
8450
+ }
8451
+ if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
8452
+ });
8453
+ for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
8454
+ }
8455
+ return invokedFunctions;
8456
+ };
8457
+ //#endregion
8409
8458
  //#region src/plugin/utils/is-react-hook-name.ts
8410
8459
  const isReactHookName = (name) => {
8411
8460
  if (!name.startsWith("use")) return false;
@@ -8461,46 +8510,90 @@ const enclosingComponentOrHookName = (node) => {
8461
8510
  return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
8462
8511
  };
8463
8512
  //#endregion
8513
+ //#region src/plugin/utils/find-declarator-for-binding.ts
8514
+ const findDeclaratorForBinding = (bindingIdentifier) => {
8515
+ let ancestor = bindingIdentifier.parent;
8516
+ while (ancestor) {
8517
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
8518
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
8519
+ ancestor = ancestor.parent ?? null;
8520
+ }
8521
+ return null;
8522
+ };
8523
+ //#endregion
8524
+ //#region src/plugin/utils/executes-during-render.ts
8525
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
8526
+ "map",
8527
+ "filter",
8528
+ "forEach",
8529
+ "flatMap",
8530
+ "reduce",
8531
+ "reduceRight",
8532
+ "some",
8533
+ "every",
8534
+ "find",
8535
+ "findIndex",
8536
+ "findLast",
8537
+ "findLastIndex",
8538
+ "sort",
8539
+ "toSorted"
8540
+ ]);
8541
+ const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
8542
+ const isGlobalArrayFromMember = (node, scopes) => {
8543
+ const unwrappedNode = stripParenExpression(node);
8544
+ if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
8545
+ return unwrappedNode.property.name === "from";
8546
+ };
8547
+ const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
8548
+ const unwrappedNode = stripParenExpression(node);
8549
+ if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
8550
+ const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
8551
+ if (!binding?.initializer) return false;
8552
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
8553
+ return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
8554
+ };
8555
+ const executesDuringRender = (functionNode, scopes) => {
8556
+ const parent = functionNode.parent;
8557
+ if (!isNodeOfType(parent, "CallExpression")) return false;
8558
+ if (parent.callee === functionNode) return true;
8559
+ if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
8560
+ if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
8561
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
8562
+ };
8563
+ //#endregion
8564
+ //#region src/plugin/utils/find-render-phase-component-or-hook.ts
8565
+ const findRenderPhaseComponentOrHook = (node, scopes) => {
8566
+ let functionNode = findEnclosingFunction(node);
8567
+ while (functionNode) {
8568
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
8569
+ if (!executesDuringRender(functionNode, scopes)) return null;
8570
+ functionNode = findEnclosingFunction(functionNode);
8571
+ }
8572
+ return null;
8573
+ };
8574
+ //#endregion
8575
+ //#region src/plugin/utils/get-function-binding-name.ts
8576
+ const getFunctionBindingIdentifier = (functionNode) => {
8577
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
8578
+ const parent = functionNode.parent;
8579
+ if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
8580
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
8581
+ if (isNodeOfType(parent, "CallExpression")) {
8582
+ const callParent = parent.parent;
8583
+ if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
8584
+ }
8585
+ return null;
8586
+ };
8587
+ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
8588
+ //#endregion
8464
8589
  //#region src/plugin/utils/get-range-start.ts
8465
8590
  const getRangeStart = (node) => {
8466
8591
  const rangeStart = node.range?.[0];
8467
8592
  return typeof rangeStart === "number" ? rangeStart : null;
8468
8593
  };
8469
8594
  //#endregion
8470
- //#region src/plugin/utils/is-result-discarded-call.ts
8471
- const isResultDiscardedCall = (callExpression) => {
8472
- let node = callExpression;
8473
- let parent = node.parent;
8474
- while (parent) {
8475
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
8476
- if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
8477
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
8478
- if (isNodeOfType(parent, "ChainExpression")) {
8479
- node = parent;
8480
- parent = node.parent;
8481
- continue;
8482
- }
8483
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
8484
- node = parent;
8485
- parent = node.parent;
8486
- continue;
8487
- }
8488
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
8489
- node = parent;
8490
- parent = node.parent;
8491
- continue;
8492
- }
8493
- if (isNodeOfType(parent, "SequenceExpression")) {
8494
- const expressions = parent.expressions ?? [];
8495
- if (expressions[expressions.length - 1] !== node) return true;
8496
- node = parent;
8497
- parent = node.parent;
8498
- continue;
8499
- }
8500
- return false;
8501
- }
8502
- return false;
8503
- };
8595
+ //#region src/plugin/utils/is-event-handler-attribute.ts
8596
+ const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
8504
8597
  //#endregion
8505
8598
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
8506
8599
  const walkInsideStatementBlocks = (node, visitor) => {
@@ -8535,125 +8628,9 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
8535
8628
  return true;
8536
8629
  };
8537
8630
  //#endregion
8538
- //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
8539
- const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
8540
- "each",
8541
- "every",
8542
- "filter",
8543
- "find",
8544
- "findIndex",
8545
- "findLast",
8546
- "findLastIndex",
8547
- "flatMap",
8548
- "forEach",
8549
- "map",
8550
- "reduce",
8551
- "reduceRight",
8552
- "some",
8553
- "sort",
8554
- "toSorted"
8555
- ]);
8556
- const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
8557
- "from",
8558
- "fromAsync",
8559
- "groupBy"
8560
- ]);
8561
- const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
8562
- const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
8563
- const isListenerRemovalViaNullHandler = (callNode) => {
8564
- if (!isNodeOfType(callNode, "CallExpression")) return false;
8565
- const callee = unwrapChainExpression$3(callNode.callee);
8566
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
8567
- };
8568
- const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
8569
- "off",
8570
- "removeEventListener",
8571
- "removeListener"
8572
- ]);
8573
- const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
8574
- if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
8575
- const handlerArgument = callNode.arguments?.[1];
8576
- return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
8577
- };
8578
- const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8579
- const callNode = unwrapChainExpression$3(node);
8580
- if (!isNodeOfType(callNode, "CallExpression")) return false;
8581
- if (isListenerRemovalViaNullHandler(callNode)) return true;
8582
- const callee = unwrapChainExpression$3(callNode.callee);
8583
- if (isNodeOfType(callee, "Identifier")) {
8584
- if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
8585
- if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
8586
- if (knownCleanupFunctionNames.has(callee.name)) return true;
8587
- return false;
8588
- }
8589
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
8590
- if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
8591
- if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
8592
- return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
8593
- }
8594
- return false;
8595
- };
8596
- const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
8597
- const isIteratorCallbackArgument = (node) => {
8598
- const parentNode = node.parent;
8599
- if (!isNodeOfType(parentNode, "CallExpression")) return false;
8600
- if (!parentNode.arguments?.some((argument) => argument === node)) return false;
8601
- const callee = unwrapChainExpression$3(parentNode.callee);
8602
- if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
8603
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
8604
- };
8605
- const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8606
- let didFindRelease = false;
8607
- walkAst(node, (child) => {
8608
- if (didFindRelease) return false;
8609
- if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
8610
- if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
8611
- didFindRelease = true;
8612
- return false;
8613
- }
8614
- });
8615
- return didFindRelease;
8616
- };
8617
- const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8618
- if (!isFunctionLike$1(node)) return false;
8619
- return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
8620
- };
8621
- const isProvablyNoOpCleanupFunction = (node) => {
8622
- if (!isFunctionLike$1(node)) return false;
8623
- let sawNoOpRemoval = false;
8624
- let sawOtherCall = false;
8625
- walkAst(node.body, (child) => {
8626
- if (sawOtherCall) return false;
8627
- if (isFunctionLike$1(child)) return false;
8628
- const callNode = unwrapChainExpression$3(child);
8629
- if (!isNodeOfType(callNode, "CallExpression")) return;
8630
- const callee = unwrapChainExpression$3(callNode.callee);
8631
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
8632
- sawNoOpRemoval = true;
8633
- return;
8634
- }
8635
- sawOtherCall = true;
8636
- });
8637
- return sawNoOpRemoval && !sawOtherCall;
8638
- };
8639
- const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
8640
- if (!returnedValue) return false;
8641
- const unwrappedValue = unwrapChainExpression$3(returnedValue);
8642
- if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
8643
- if (isNodeOfType(unwrappedValue, "Identifier")) {
8644
- if (unwrappedValue.name === "undefined") return false;
8645
- if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
8646
- return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
8647
- }
8648
- if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
8649
- if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
8650
- if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
8651
- if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
8652
- return false;
8653
- };
8654
- //#endregion
8655
8631
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
8656
8632
  const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
8633
+ const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
8657
8634
  const RESOURCE_NOUN_BY_KIND = {
8658
8635
  subscribe: "subscription",
8659
8636
  timer: "timer",
@@ -8664,7 +8641,66 @@ const isSubscribeOrObserveCall = (node) => {
8664
8641
  if (isSubscribeLikeCallExpression(node)) return true;
8665
8642
  return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
8666
8643
  };
8667
- const findSubscribeLikeUsages = (callback) => {
8644
+ const resolveExpressionKey$1 = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
8645
+ if (!expression) return null;
8646
+ const unwrappedExpression = stripParenExpression(expression);
8647
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
8648
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
8649
+ if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
8650
+ if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
8651
+ visitedSymbolIds.add(symbol.id);
8652
+ const bindingProperty = symbol.bindingIdentifier.parent;
8653
+ const bindingPattern = bindingProperty?.parent;
8654
+ const variableDeclarator = bindingPattern?.parent;
8655
+ const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
8656
+ if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
8657
+ const objectKey = resolveExpressionKey$1(variableDeclarator.init, context, visitedSymbolIds);
8658
+ return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
8659
+ }
8660
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
8661
+ if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey$1(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
8662
+ return `symbol:${symbol.id}`;
8663
+ }
8664
+ if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
8665
+ if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
8666
+ const objectKey = resolveExpressionKey$1(unwrappedExpression.object, context, visitedSymbolIds);
8667
+ return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
8668
+ }
8669
+ if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
8670
+ if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
8671
+ if (isFunctionLike$1(unwrappedExpression)) {
8672
+ const rangeStart = getRangeStart(unwrappedExpression);
8673
+ return rangeStart === null ? null : `function:${rangeStart}`;
8674
+ }
8675
+ return null;
8676
+ };
8677
+ const findAssignedResourceKey = (resourceNode, context) => {
8678
+ let currentNode = resourceNode;
8679
+ let parentNode = currentNode.parent;
8680
+ while (isNodeOfType(parentNode, "ChainExpression")) {
8681
+ currentNode = parentNode;
8682
+ parentNode = currentNode.parent;
8683
+ }
8684
+ if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
8685
+ if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey$1(parentNode.left, context);
8686
+ return null;
8687
+ };
8688
+ const getCallRegistrationDetails = (callNode, context) => {
8689
+ const callee = stripParenExpression(callNode.callee);
8690
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return {
8691
+ receiverKey: null,
8692
+ registrationVerbName: null,
8693
+ eventKey: null,
8694
+ handlerKey: null
8695
+ };
8696
+ return {
8697
+ receiverKey: resolveExpressionKey$1(callee.object, context),
8698
+ registrationVerbName: callee.property.name,
8699
+ eventKey: resolveExpressionKey$1(callNode.arguments?.[0], context),
8700
+ handlerKey: resolveExpressionKey$1(callNode.arguments?.[1], context)
8701
+ };
8702
+ };
8703
+ const findSubscribeLikeUsages = (callback, context) => {
8668
8704
  const usages = [];
8669
8705
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8670
8706
  let cleanupArgument = null;
@@ -8673,13 +8709,22 @@ const findSubscribeLikeUsages = (callback) => {
8673
8709
  const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
8674
8710
  if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
8675
8711
  }
8712
+ const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
8676
8713
  walkAst(callback, (child) => {
8677
- if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
8714
+ if (child !== callback && isFunctionLike$1(child)) {
8715
+ if (child === cleanupArgument) return false;
8716
+ if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
8717
+ }
8678
8718
  if (isSocketConstruction(child)) {
8679
8719
  usages.push({
8680
8720
  kind: "socket",
8681
8721
  node: child,
8682
- resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8722
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
8723
+ handleKey: findAssignedResourceKey(child, context),
8724
+ receiverKey: null,
8725
+ registrationVerbName: null,
8726
+ eventKey: null,
8727
+ handlerKey: null
8683
8728
  });
8684
8729
  return;
8685
8730
  }
@@ -8688,118 +8733,348 @@ const findSubscribeLikeUsages = (callback) => {
8688
8733
  usages.push({
8689
8734
  kind: "timer",
8690
8735
  node: child,
8691
- resourceName: child.callee.name
8736
+ resourceName: child.callee.name,
8737
+ handleKey: findAssignedResourceKey(child, context),
8738
+ receiverKey: null,
8739
+ registrationVerbName: child.callee.name,
8740
+ eventKey: null,
8741
+ handlerKey: null
8692
8742
  });
8693
8743
  return;
8694
8744
  }
8695
- 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)) usages.push({
8696
- kind: "subscribe",
8697
- node: child,
8698
- resourceName: child.callee.property.name
8699
- });
8700
- });
8701
- return usages;
8702
- };
8703
- const collectCleanupBindings = (effectCallback) => {
8704
- const bindings = {
8705
- cleanupFunctionNames: /* @__PURE__ */ new Set(),
8706
- subscriptionNames: /* @__PURE__ */ new Set(),
8707
- effectScopeVariableNames: /* @__PURE__ */ new Set()
8708
- };
8709
- if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
8710
- if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
8711
- walkAst(effectCallback.body, (child) => {
8712
- if (!isSubscribeOrObserveCall(child)) return;
8713
- if (!isNodeOfType(child, "CallExpression")) return;
8714
- if (!isNodeOfType(child.callee, "MemberExpression")) return;
8715
- if (!isNodeOfType(child.callee.object, "Identifier")) return;
8716
- bindings.subscriptionNames.add(child.callee.object.name);
8717
- });
8718
- walkInsideStatementBlocks(effectCallback.body, (child) => {
8719
- if (!isNodeOfType(child, "VariableDeclaration")) return;
8720
- for (const declarator of child.declarations ?? []) {
8721
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
8722
- const bindingName = declarator.id.name;
8723
- bindings.effectScopeVariableNames.add(bindingName);
8724
- const init = declarator.init;
8725
- if (!init) continue;
8726
- if (isSocketConstruction(init)) {
8727
- bindings.subscriptionNames.add(bindingName);
8728
- continue;
8729
- }
8730
- if (!isNodeOfType(init, "CallExpression")) continue;
8731
- if (isSubscribeLikeCallExpression(init)) {
8732
- bindings.subscriptionNames.add(bindingName);
8733
- if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
8734
- }
8735
- }
8736
- });
8737
- walkAst(effectCallback.body, (child) => {
8738
- if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
8739
- if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
8740
- bindings.cleanupFunctionNames.add(child.id.name);
8741
- return false;
8742
- }
8743
- });
8744
- walkInsideStatementBlocks(effectCallback.body, (child) => {
8745
- if (!isNodeOfType(child, "VariableDeclaration")) return;
8746
- for (const declarator of child.declarations ?? []) {
8747
- if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
8748
- if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
8745
+ 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)) {
8746
+ const registrationDetails = getCallRegistrationDetails(child, context);
8747
+ usages.push({
8748
+ kind: "subscribe",
8749
+ node: child,
8750
+ resourceName: child.callee.property.name,
8751
+ handleKey: findAssignedResourceKey(child, context),
8752
+ ...registrationDetails
8753
+ });
8749
8754
  }
8750
8755
  });
8751
- walkAst(effectCallback.body, (child) => {
8752
- if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && bindings.effectScopeVariableNames.has(child.left.name) && isCleanupFunctionLike(child.right, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(child.left.name);
8753
- });
8754
- return bindings;
8755
- };
8756
- const removeSynchronouslyReleasedUsages = (callback, usages) => {
8756
+ return usages.filter((usage) => isNodeReachableWithinFunction(usage.node, context));
8757
+ };
8758
+ const isNodeReachableWithinFunction = (node, context) => {
8759
+ const owner = context.cfg.enclosingFunction(node);
8760
+ if (!owner) return true;
8761
+ const functionCfg = context.cfg.cfgFor(owner);
8762
+ if (!functionCfg) return true;
8763
+ const targetBlock = functionCfg.blockOf(node);
8764
+ if (!targetBlock) return true;
8765
+ const visitedBlocks = new Set([functionCfg.entry]);
8766
+ const pendingBlocks = [functionCfg.entry];
8767
+ while (pendingBlocks.length > 0) {
8768
+ const currentBlock = pendingBlocks.pop();
8769
+ if (!currentBlock) break;
8770
+ if (currentBlock === targetBlock) return true;
8771
+ for (const edge of currentBlock.successors) {
8772
+ if (visitedBlocks.has(edge.to)) continue;
8773
+ visitedBlocks.add(edge.to);
8774
+ pendingBlocks.push(edge.to);
8775
+ }
8776
+ }
8777
+ return false;
8778
+ };
8779
+ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, context) => {
8780
+ let pathAnchor = usageNode;
8781
+ let pathOwner = findEnclosingFunction(pathAnchor);
8782
+ while (pathOwner && isSynchronousIteratorCallback(pathOwner)) {
8783
+ if (matchingNodes.length > 0 && matchingNodes.every((matchingNode) => context.cfg.enclosingFunction(matchingNode) === pathOwner)) break;
8784
+ const iteratorCall = pathOwner.parent;
8785
+ if (!isNodeOfType(iteratorCall, "CallExpression")) break;
8786
+ pathAnchor = iteratorCall;
8787
+ pathOwner = findEnclosingFunction(pathAnchor);
8788
+ }
8789
+ const owner = context.cfg.enclosingFunction(pathAnchor);
8790
+ if (!owner) return false;
8791
+ const functionCfg = context.cfg.cfgFor(owner);
8792
+ if (!functionCfg) return false;
8793
+ const usageBlock = functionCfg.blockOf(pathAnchor);
8794
+ if (!usageBlock) return false;
8795
+ const usageStart = getRangeStart(usageNode);
8796
+ const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
8797
+ if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
8798
+ const matchingBlock = functionCfg.blockOf(matchingNode);
8799
+ if (!matchingBlock) return [];
8800
+ const matchingStart = getRangeStart(matchingNode);
8801
+ if (matchingBlock === usageBlock && usageStart !== null && matchingStart !== null && matchingStart < usageStart) return [];
8802
+ return [matchingBlock];
8803
+ }));
8804
+ if (matchingBlocks.has(usageBlock)) return true;
8805
+ const visitedBlocks = new Set([usageBlock]);
8806
+ const pendingBlocks = [usageBlock];
8807
+ while (pendingBlocks.length > 0) {
8808
+ const currentBlock = pendingBlocks.pop();
8809
+ if (!currentBlock) break;
8810
+ for (const edge of currentBlock.successors) {
8811
+ if (matchingBlocks.has(edge.to)) continue;
8812
+ if (edge.to === functionCfg.exit) return false;
8813
+ if (visitedBlocks.has(edge.to)) continue;
8814
+ visitedBlocks.add(edge.to);
8815
+ pendingBlocks.push(edge.to);
8816
+ }
8817
+ }
8818
+ return matchingBlocks.size > 0;
8819
+ };
8820
+ const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
8757
8821
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8758
8822
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8759
- const releaseStarts = [];
8823
+ const releaseCalls = [];
8760
8824
  walkInsideStatementBlocks(callback.body, (child) => {
8761
- if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8762
- const releaseStart = getRangeStart(child);
8763
- if (releaseStart !== null) releaseStarts.push(releaseStart);
8825
+ if (!isNodeOfType(isNodeOfType(child, "ChainExpression") ? child.expression : child, "CallExpression")) return;
8826
+ releaseCalls.push(child);
8764
8827
  });
8765
- if (releaseStarts.length === 0) return usages;
8828
+ if (releaseCalls.length === 0) return usages;
8766
8829
  return usages.filter((usage) => {
8767
8830
  const usageStart = getRangeStart(usage.node);
8768
8831
  if (usageStart === null) return true;
8769
- return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8832
+ const matchingReleaseCalls = releaseCalls.filter((releaseCall) => {
8833
+ const releaseStart = getRangeStart(releaseCall);
8834
+ return releaseStart !== null && releaseStart > usageStart && doesReleaseCallMatchUsage(releaseCall, usage, context);
8835
+ });
8836
+ return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
8770
8837
  });
8771
8838
  };
8772
- const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
8773
- if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
8774
- const returnStart = getRangeStart(returnStatement);
8775
- if (returnStart === null) return true;
8776
- return usages.some((usage) => {
8777
- const usageStart = getRangeStart(usage.node);
8778
- return usageStart === null || usageStart < returnStart;
8839
+ const resolveIteratorCollectionKey = (expression, context) => {
8840
+ if (!expression || !isNodeOfType(expression, "Identifier")) return null;
8841
+ const symbol = context.scopes.symbolFor(expression);
8842
+ if (!symbol || symbol.kind !== "parameter") return null;
8843
+ let callbackNode = symbol.bindingIdentifier.parent;
8844
+ while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
8845
+ if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
8846
+ const callNode = callbackNode.parent;
8847
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8848
+ const callee = stripParenExpression(callNode.callee);
8849
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
8850
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return resolveExpressionKey$1(callNode.arguments[0], context);
8851
+ if (callNode.arguments?.[0] === callbackNode) return resolveExpressionKey$1(callee.object, context);
8852
+ }
8853
+ return null;
8854
+ };
8855
+ const findCollectionMappingCall = (callbackNode) => {
8856
+ if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
8857
+ const callNode = callbackNode.parent;
8858
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8859
+ const callee = stripParenExpression(callNode.callee);
8860
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return null;
8861
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return callNode;
8862
+ return callee.property.name === "map" && callNode.arguments?.[0] === callbackNode ? callNode : null;
8863
+ };
8864
+ const findMappedResourceCollectionKey = (resourceNode, context) => {
8865
+ const callbackNode = findEnclosingFunction(resourceNode);
8866
+ if (!callbackNode || !isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression")) return null;
8867
+ const mappingCall = findCollectionMappingCall(callbackNode);
8868
+ if (!mappingCall) return null;
8869
+ if (isNodeOfType(callbackNode.body, "BlockStatement")) {
8870
+ const resourceRoot = findTransparentExpressionRoot(resourceNode);
8871
+ const resourceDeclarator = resourceRoot.parent;
8872
+ const resourceDeclaration = resourceDeclarator?.parent;
8873
+ if (!isNodeOfType(resourceDeclarator, "VariableDeclarator") || resourceDeclarator.init !== resourceRoot || !isNodeOfType(resourceDeclarator.id, "Identifier") || !isNodeOfType(resourceDeclaration, "VariableDeclaration") || resourceDeclaration.kind !== "const" || resourceDeclaration.parent !== callbackNode.body) return null;
8874
+ const returnStatements = [];
8875
+ walkAst(callbackNode.body, (child) => {
8876
+ if (child !== callbackNode.body && isFunctionLike$1(child)) return false;
8877
+ if (isNodeOfType(child, "ReturnStatement")) returnStatements.push(child);
8878
+ });
8879
+ const returnStatement = returnStatements[0];
8880
+ const callbackStatements = callbackNode.body.body ?? [];
8881
+ const returnedIdentifier = isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument ? stripParenExpression(returnStatement.argument) : null;
8882
+ const resourceSymbol = context.scopes.symbolFor(resourceDeclarator.id);
8883
+ if (returnStatements.length !== 1 || callbackStatements[callbackStatements.length - 1] !== returnStatement || !isNodeOfType(returnedIdentifier, "Identifier") || !resourceSymbol || context.scopes.symbolFor(returnedIdentifier)?.id !== resourceSymbol.id || !doMatchingNodesCoverEveryPathAfterUsage(resourceNode, [returnStatement], context)) return null;
8884
+ } else if (findTransparentExpressionRoot(resourceNode) !== callbackNode.body) return null;
8885
+ const mappingRoot = findTransparentExpressionRoot(mappingCall);
8886
+ const collectionDeclarator = mappingRoot.parent;
8887
+ return isNodeOfType(collectionDeclarator, "VariableDeclarator") && collectionDeclarator.init === mappingRoot ? resolveExpressionKey$1(collectionDeclarator.id, context) : null;
8888
+ };
8889
+ const findContainingCollectionKey = (resourceNode, context) => {
8890
+ const mappedCollectionKey = findMappedResourceCollectionKey(resourceNode, context);
8891
+ if (mappedCollectionKey !== null) return mappedCollectionKey;
8892
+ let currentNode = resourceNode;
8893
+ let parentNode = currentNode.parent;
8894
+ while (parentNode) {
8895
+ if (isFunctionLike$1(parentNode)) return null;
8896
+ if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
8897
+ currentNode = parentNode;
8898
+ parentNode = currentNode.parent;
8899
+ }
8900
+ return null;
8901
+ };
8902
+ const isWithinAssignmentTarget = (identifier) => {
8903
+ let currentNode = identifier;
8904
+ let parentNode = currentNode.parent;
8905
+ while (parentNode) {
8906
+ if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
8907
+ if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
8908
+ if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
8909
+ currentNode = parentNode;
8910
+ parentNode = currentNode.parent;
8911
+ }
8912
+ return false;
8913
+ };
8914
+ const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
8915
+ if (!expression) return null;
8916
+ const unwrappedExpression = stripParenExpression(expression);
8917
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
8918
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
8919
+ const isUnreassignedMutableBinding = (symbol?.kind === "let" || symbol?.kind === "var") && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && symbol.declarationNode.id === symbol.bindingIdentifier && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
8920
+ const recursiveFunctionSymbol = symbol?.kind === "function" && isFunctionLike$1(symbol.declarationNode) ? context.scopes.ownScopeFor(symbol.declarationNode)?.symbols.find((candidate) => candidate.name === symbol.name && candidate.declarationNode === symbol.declarationNode) : null;
8921
+ const isUnreassignedFunctionBinding = symbol?.kind === "function" && symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier)) && (!recursiveFunctionSymbol || recursiveFunctionSymbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) && symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length === 1;
8922
+ if (!symbol || symbol.kind !== "const" && !isUnreassignedMutableBinding && !isUnreassignedFunctionBinding || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
8923
+ visitedSymbolIds.add(symbol.id);
8924
+ return resolveStableValue(symbol.initializer, context, visitedSymbolIds);
8925
+ };
8926
+ const resolveObjectExpression = (expression, context) => {
8927
+ const resolvedExpression = resolveStableValue(expression, context);
8928
+ return isNodeOfType(resolvedExpression, "ObjectExpression") ? resolvedExpression : null;
8929
+ };
8930
+ const getListenerAbortControllerKey = (usage, context) => {
8931
+ if (usage.registrationVerbName !== "addEventListener" || !isNodeOfType(usage.node, "CallExpression")) return null;
8932
+ const optionsArgument = usage.node.arguments?.[2];
8933
+ const optionsObject = resolveObjectExpression(optionsArgument, context);
8934
+ if (!optionsObject) return null;
8935
+ for (const property of optionsObject.properties ?? []) {
8936
+ if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== "signal") continue;
8937
+ const signalKey = resolveExpressionKey$1(property.value, context);
8938
+ return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
8939
+ }
8940
+ return null;
8941
+ };
8942
+ const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
8943
+ "every",
8944
+ "filter",
8945
+ "flatMap",
8946
+ "forEach",
8947
+ "map",
8948
+ "reduce",
8949
+ "reduceRight",
8950
+ "some"
8951
+ ]);
8952
+ const isSynchronousIteratorCallback = (functionNode) => {
8953
+ const callNode = functionNode.parent;
8954
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
8955
+ const callee = stripParenExpression(callNode.callee);
8956
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
8957
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
8958
+ return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
8959
+ };
8960
+ const findDirectCallForReference = (identifier) => {
8961
+ const expressionRoot = findTransparentExpressionRoot(identifier);
8962
+ const callNode = expressionRoot.parent;
8963
+ return isNodeOfType(callNode, "CallExpression") && callNode.callee === expressionRoot ? callNode : null;
8964
+ };
8965
+ const findSingleDirectInvocation = (functionNode, caller, context) => {
8966
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
8967
+ if (!bindingIdentifier || resolveStableValue(bindingIdentifier, context) !== functionNode) return null;
8968
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
8969
+ if (!symbol) return null;
8970
+ const invocationCalls = symbol.references.flatMap((reference) => {
8971
+ const callNode = findDirectCallForReference(reference.identifier);
8972
+ return callNode ? [callNode] : [];
8973
+ });
8974
+ if (invocationCalls.length !== 1) return null;
8975
+ const invocationCall = invocationCalls[0];
8976
+ return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
8977
+ };
8978
+ const resolveCleanupPathAnchor = (usageNode, effectCallback, context) => {
8979
+ const usageFunction = findEnclosingFunction(usageNode);
8980
+ if (!usageFunction || usageFunction === effectCallback) return usageNode;
8981
+ return findSingleDirectInvocation(usageFunction, effectCallback, context) ?? usageNode;
8982
+ };
8983
+ const resolveSingleAssignedCleanupFunction = (expression, usage, context) => {
8984
+ const unwrappedExpression = stripParenExpression(expression);
8985
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
8986
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
8987
+ const initializer = symbol?.initializer ? stripParenExpression(symbol.initializer) : null;
8988
+ if (!symbol || symbol.kind !== "let" && symbol.kind !== "var" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || !isNodeOfType(initializer, "Literal") || initializer.value !== null || symbol.scope.symbols.filter((candidate) => candidate.name === symbol.name).length !== 1) return null;
8989
+ const assignmentReferences = symbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
8990
+ if (assignmentReferences.length !== 1) return null;
8991
+ const assignmentReference = assignmentReferences[0];
8992
+ const assignmentTarget = findTransparentExpressionRoot(assignmentReference.identifier);
8993
+ const assignmentNode = assignmentTarget.parent;
8994
+ if (!isNodeOfType(assignmentNode, "AssignmentExpression") || assignmentNode.operator !== "=" || assignmentNode.left !== assignmentTarget || findEnclosingFunction(assignmentNode) !== findEnclosingFunction(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [assignmentNode], context)) return null;
8995
+ const assignedValue = stripParenExpression(assignmentNode.right);
8996
+ return isFunctionLike$1(assignedValue) ? assignedValue : null;
8997
+ };
8998
+ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visitedFunctions = /* @__PURE__ */ new Set()) => {
8999
+ if (!isFunctionLike$1(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
9000
+ visitedFunctions.add(cleanupFunction);
9001
+ let didCleanupFunctionMatch = false;
9002
+ walkAst(cleanupFunction.body, (cleanupChild) => {
9003
+ if (didCleanupFunctionMatch) return false;
9004
+ if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
9005
+ if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
9006
+ didCleanupFunctionMatch = true;
9007
+ return false;
9008
+ }
9009
+ const helperCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
9010
+ if (!isNodeOfType(helperCall, "CallExpression")) return;
9011
+ const stableHelperFunction = resolveStableValue(helperCall.callee, context);
9012
+ const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
9013
+ if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions)) {
9014
+ didCleanupFunctionMatch = true;
9015
+ return false;
9016
+ }
8779
9017
  });
9018
+ return didCleanupFunctionMatch;
8780
9019
  };
8781
- const effectHasCleanupReturn = (callback, usages) => {
9020
+ const effectHasCleanupForUsage = (callback, usage, context) => {
8782
9021
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
8783
- if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
8784
- const cleanupBindings = collectCleanupBindings(callback);
8785
- let didFindCleanupReturn = false;
9022
+ if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
9023
+ if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
9024
+ const matchingCleanupReturns = [];
8786
9025
  walkInsideStatementBlocks(callback.body, (child) => {
8787
- if (didFindCleanupReturn) return;
8788
9026
  if (!isNodeOfType(child, "ReturnStatement")) return;
8789
- if (!cleanupReturnRunsAfterUsage(child, usages)) return;
8790
- if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
9027
+ const returnStart = getRangeStart(child);
9028
+ const usageStart = getRangeStart(usage.node);
9029
+ if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9030
+ const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9031
+ if (!returnedValue) return;
9032
+ if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9033
+ matchingCleanupReturns.push(child);
9034
+ return;
9035
+ }
9036
+ if (usage.kind === "subscribe" && isNodeOfType(returnedValue, "Identifier") && usage.handleKey !== null && resolveExpressionKey$1(returnedValue, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) {
9037
+ matchingCleanupReturns.push(child);
9038
+ return;
9039
+ }
9040
+ if (isNodeOfType(returnedValue, "Identifier")) {
9041
+ if (returnedValue.name === "undefined" && context.scopes.isGlobalReference(returnedValue)) return;
9042
+ const returnedKey = resolveExpressionKey$1(returnedValue, context);
9043
+ if (usage.handleKey !== null && returnedKey === usage.handleKey) return;
9044
+ if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9045
+ }
9046
+ const cleanupFunction = resolveStableValue(returnedValue, context);
9047
+ if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9048
+ if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
8791
9049
  });
8792
- return didFindCleanupReturn;
9050
+ return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9051
+ };
9052
+ const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9053
+ for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
9054
+ return null;
9055
+ };
9056
+ const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
9057
+ const unwrappedExpression = stripParenExpression(expression);
9058
+ if (isNodeOfType(unwrappedExpression, "NewExpression") && isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "AbortController") return true;
9059
+ if (isNodeOfType(unwrappedExpression, "MemberExpression")) return isLocalAbortControllerExpression(unwrappedExpression.object, context, visitedSymbolIds);
9060
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
9061
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
9062
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
9063
+ visitedSymbolIds.add(symbol.id);
9064
+ if (symbol.initializer) return isLocalAbortControllerExpression(symbol.initializer, context, visitedSymbolIds);
9065
+ const bindingProperty = symbol.bindingIdentifier.parent;
9066
+ const bindingPattern = bindingProperty?.parent;
9067
+ const variableDeclarator = bindingPattern?.parent;
9068
+ return Boolean(isNodeOfType(bindingProperty, "Property") && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.init && isLocalAbortControllerExpression(variableDeclarator.init, context, visitedSymbolIds));
8793
9069
  };
8794
- const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8795
- const isSelfReleasingListenerOptionProperty = (property) => {
9070
+ const isSelfReleasingListenerOptionProperty = (property, context) => {
8796
9071
  if (!isNodeOfType(property, "Property")) return false;
8797
9072
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8798
- if (keyName === "signal") return true;
9073
+ if (keyName === "signal") return !isLocalAbortControllerExpression(property.value, context);
8799
9074
  if (keyName !== "once") return false;
8800
9075
  return isNodeOfType(property.value, "Literal") && property.value.value === true;
8801
9076
  };
8802
- const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
9077
+ const hasSelfReleasingListenerOptions = (node, context) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isSelfReleasingListenerOptionProperty(property, context)));
8803
9078
  const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8804
9079
  ["addEventListener", new Set(["removeEventListener", "abort"])],
8805
9080
  ["addListener", new Set([
@@ -8825,85 +9100,185 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8825
9100
  "teardown"
8826
9101
  ]);
8827
9102
  const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8828
- const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8829
9103
  const getReleaseVerbName = (node) => {
8830
- if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8831
9104
  const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8832
9105
  if (!isNodeOfType(callNode, "CallExpression")) return null;
8833
9106
  const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8834
- if (isNodeOfType(callee, "Identifier")) return callee.name;
8835
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9107
+ if (isNodeOfType(callee, "Identifier")) return TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || GLOBAL_RELEASE_METHOD_NAMES.has(callee.name) || UNIVERSAL_RELEASE_VERB_NAMES.has(callee.name) ? callee.name : null;
9108
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
9109
+ const methodName = callee.property.name;
9110
+ return GLOBAL_RELEASE_METHOD_NAMES.has(methodName) || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(methodName) || methodName === "on" ? methodName : null;
9111
+ }
8836
9112
  return null;
8837
9113
  };
9114
+ const doesReleaseCallMatchUsage = (node, usage, context) => {
9115
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
9116
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
9117
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
9118
+ if (usage.kind === "timer") {
9119
+ const expectedCleanupName = usage.registrationVerbName === "setInterval" ? "clearInterval" : "clearTimeout";
9120
+ if (!isNodeOfType(callee, "Identifier") || !TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || callee.name !== expectedCleanupName) return false;
9121
+ if (usage.handleKey !== null && resolveExpressionKey$1(callNode.arguments?.[0], context) === usage.handleKey) return true;
9122
+ const collectionKey = findContainingCollectionKey(usage.node, context);
9123
+ return collectionKey !== null && collectionKey === resolveIteratorCollectionKey(callNode.arguments?.[0], context);
9124
+ }
9125
+ if (isNodeOfType(callee, "Identifier") && usage.kind === "subscribe" && usage.handleKey !== null && resolveExpressionKey$1(callee, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
9126
+ const releaseVerbName = getReleaseVerbName(callNode);
9127
+ if (!releaseVerbName) return false;
9128
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
9129
+ const releaseReceiverKey = resolveExpressionKey$1(callee.object, context);
9130
+ if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
9131
+ if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
9132
+ if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
9133
+ if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
9134
+ const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
9135
+ if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
9136
+ const releaseEventKey = resolveExpressionKey$1(callNode.arguments?.[0], context);
9137
+ if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
9138
+ const usageIteratorCollectionKey = isNodeOfType(usage.node, "CallExpression") ? resolveIteratorCollectionKey(usage.node.arguments?.[0], context) : null;
9139
+ const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
9140
+ if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
9141
+ }
9142
+ if (releaseVerbName === "on") {
9143
+ const handlerArgument = callNode.arguments?.[1];
9144
+ return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
9145
+ }
9146
+ if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
9147
+ const releaseHandler = callNode.arguments?.[1];
9148
+ if (!releaseHandler) return releaseVerbName === "off";
9149
+ return usage.handlerKey !== null && resolveExpressionKey$1(releaseHandler, context) === usage.handlerKey;
9150
+ }
9151
+ if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
9152
+ return true;
9153
+ };
8838
9154
  const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8839
- const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8840
- let didFindPairedRelease = false;
8841
- walkAst(body, (child) => {
8842
- if (didFindPairedRelease) return false;
8843
- if (child !== body && isFunctionLike$1(child)) return false;
8844
- const releaseVerbName = getReleaseVerbName(child);
8845
- if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8846
- didFindPairedRelease = true;
8847
- return false;
8848
- }
8849
- });
8850
- return didFindPairedRelease;
9155
+ const isReturnedEffectCleanupFunction = (functionNode) => {
9156
+ let currentNode = functionNode;
9157
+ let parentNode = currentNode.parent;
9158
+ while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
9159
+ currentNode = parentNode;
9160
+ parentNode = currentNode.parent;
9161
+ }
9162
+ if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
9163
+ const effectCallback = findEnclosingFunction(parentNode);
9164
+ const effectCall = effectCallback?.parent;
9165
+ return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
9166
+ };
9167
+ const isPotentiallyReachableFunction = (functionNode, context) => {
9168
+ if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
9169
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
9170
+ if (!bindingIdentifier) return false;
9171
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
9172
+ if (!symbol) return false;
9173
+ return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
9174
+ };
9175
+ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
9176
+ if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
9177
+ const releaseFunction = findEnclosingFunction(releaseNode);
9178
+ if (!releaseFunction) return true;
9179
+ if (releaseFunction === findEnclosingFunction(usage.node)) return true;
9180
+ return isPotentiallyReachableFunction(releaseFunction, context);
8851
9181
  };
8852
- const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8853
- const collectFileReleaseVerbNames = (anyNode) => {
8854
- let programNode = anyNode;
9182
+ const fileContainsReleaseForUsage = (usage, context) => {
9183
+ let programNode = usage.node;
8855
9184
  while (programNode.parent) programNode = programNode.parent;
8856
- const cached = fileReleaseVerbNamesCache.get(programNode);
8857
- if (cached) return cached;
8858
- const releaseVerbNames = /* @__PURE__ */ new Set();
9185
+ let didFindRelease = false;
8859
9186
  walkAst(programNode, (child) => {
8860
- const releaseVerbName = getReleaseVerbName(child);
8861
- if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
9187
+ if (didFindRelease) return false;
9188
+ if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
9189
+ didFindRelease = true;
9190
+ return false;
9191
+ }
8862
9192
  });
8863
- fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8864
- return releaseVerbNames;
9193
+ return didFindRelease;
8865
9194
  };
8866
- const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8867
- const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8868
- const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8869
- if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8870
- for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
9195
+ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
9196
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
9197
+ if (!bindingIdentifier) return false;
9198
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
9199
+ const isSubscribeBinding = (candidateBinding) => {
9200
+ const symbol = context.scopes.symbolFor(candidateBinding);
9201
+ if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
9202
+ visitedSymbolIds.add(symbol.id);
9203
+ return symbol.references.every((reference) => {
9204
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
9205
+ const referenceParent = referenceRoot.parent;
9206
+ if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
9207
+ const aliasDeclaration = referenceParent?.parent;
9208
+ return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
9209
+ });
9210
+ };
9211
+ return isSubscribeBinding(bindingIdentifier);
9212
+ };
9213
+ const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
9214
+ let currentNode = resourceNode;
9215
+ let parentNode = currentNode.parent;
9216
+ while (parentNode) {
9217
+ if (isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode) return true;
9218
+ if (isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode && allowConciseReturnEscape) return true;
9219
+ if (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
9220
+ currentNode = parentNode;
9221
+ parentNode = currentNode.parent;
9222
+ continue;
9223
+ }
9224
+ return false;
9225
+ }
8871
9226
  return false;
8872
9227
  };
8873
- const findRetainedFunctionLeak = (retainedFunction) => {
9228
+ const findRetainedFunctionLeak = (retainedFunction, context) => {
8874
9229
  if (!isFunctionLike$1(retainedFunction)) return null;
8875
9230
  const body = retainedFunction.body;
8876
9231
  if (!body) return null;
8877
- const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8878
9232
  let leak = null;
9233
+ const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
9234
+ const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
9235
+ const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
8879
9236
  walkAst(body, (child) => {
8880
9237
  if (leak !== null) return false;
8881
9238
  if (isFunctionLike$1(child)) return false;
8882
- if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8883
- if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8884
- leak = {
9239
+ if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
9240
+ const socketUsage = {
8885
9241
  kind: "socket",
8886
9242
  node: child,
8887
- resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
9243
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
9244
+ handleKey: findAssignedResourceKey(child, context),
9245
+ receiverKey: null,
9246
+ registrationVerbName: null,
9247
+ eventKey: null,
9248
+ handlerKey: null
8888
9249
  };
8889
- return false;
9250
+ if (!hasReleaseForUsage(socketUsage)) {
9251
+ leak = socketUsage;
9252
+ return false;
9253
+ }
8890
9254
  }
8891
9255
  if (!isNodeOfType(child, "CallExpression")) return;
8892
- if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8893
- leak = {
9256
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
9257
+ const timerUsage = {
8894
9258
  kind: "timer",
8895
9259
  node: child,
8896
- resourceName: "setInterval"
9260
+ resourceName: "setInterval",
9261
+ handleKey: findAssignedResourceKey(child, context),
9262
+ receiverKey: null,
9263
+ registrationVerbName: "setInterval",
9264
+ eventKey: null,
9265
+ handlerKey: null
8897
9266
  };
8898
- return false;
9267
+ if (!hasReleaseForUsage(timerUsage)) {
9268
+ leak = timerUsage;
9269
+ return false;
9270
+ }
8899
9271
  }
8900
- if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8901
- const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8902
- if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
9272
+ if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
9273
+ const registrationDetails = getCallRegistrationDetails(child, context);
9274
+ const subscriptionUsage = {
8903
9275
  kind: "subscribe",
8904
9276
  node: child,
8905
- resourceName: registrationVerbName
9277
+ resourceName: registrationDetails.registrationVerbName ?? "subscribe",
9278
+ handleKey: findAssignedResourceKey(child, context),
9279
+ ...registrationDetails
8906
9280
  };
9281
+ if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
8907
9282
  return false;
8908
9283
  }
8909
9284
  });
@@ -8915,6 +9290,26 @@ const isRetainedComponentScopeFunction = (functionNode) => {
8915
9290
  if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8916
9291
  return enclosingComponentOrHookName(functionNode) !== null;
8917
9292
  };
9293
+ const isDirectJsxEventHandlerValue = (expression) => {
9294
+ const expressionRoot = findTransparentExpressionRoot(expression);
9295
+ const expressionContainer = expressionRoot.parent;
9296
+ return isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === expressionRoot && isEventHandlerAttribute(expressionContainer.parent);
9297
+ };
9298
+ const isInlineRetainedHandlerFunction = (functionNode, context) => {
9299
+ if (!isFunctionLike$1(functionNode)) return false;
9300
+ const functionRoot = findTransparentExpressionRoot(functionNode);
9301
+ const callbackCall = functionRoot.parent;
9302
+ if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
9303
+ const parentNode = functionNode.parent;
9304
+ if (isDirectJsxEventHandlerValue(functionNode)) return true;
9305
+ if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
9306
+ const propertyName = getStaticPropertyKeyName(parentNode);
9307
+ if (!propertyName || !/^on[A-Z]/.test(propertyName)) return false;
9308
+ const objectExpression = parentNode.parent;
9309
+ if (!isNodeOfType(objectExpression, "ObjectExpression")) return false;
9310
+ const objectParent = objectExpression.parent;
9311
+ return (isNodeOfType(objectParent, "CallExpression") && objectParent.arguments.some((argument) => argument === objectExpression) || isNodeOfType(objectParent, "JSXExpressionContainer")) && findRenderPhaseComponentOrHook(parentNode, context.scopes) !== null;
9312
+ };
8918
9313
  const effectNeedsCleanup = defineRule({
8919
9314
  id: "effect-needs-cleanup",
8920
9315
  title: "Effect subscription or timer never cleaned up",
@@ -8923,7 +9318,8 @@ const effectNeedsCleanup = defineRule({
8923
9318
  recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
8924
9319
  create: (context) => {
8925
9320
  const reportRetainedLeak = (retainedFunction) => {
8926
- const leak = findRetainedFunctionLeak(retainedFunction);
9321
+ if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
9322
+ const leak = findRetainedFunctionLeak(retainedFunction, context);
8927
9323
  if (!leak) return;
8928
9324
  const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8929
9325
  context.report({
@@ -8935,30 +9331,31 @@ const effectNeedsCleanup = defineRule({
8935
9331
  CallExpression(node) {
8936
9332
  if (isHookCall$2(node, "useCallback")) {
8937
9333
  const retainedCallback = getEffectCallback(node);
8938
- if (retainedCallback) reportRetainedLeak(retainedCallback);
9334
+ if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
8939
9335
  return;
8940
9336
  }
8941
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
9337
+ if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
8942
9338
  const callback = getEffectCallback(node);
8943
9339
  if (!callback) return;
8944
- const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
9340
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
8945
9341
  if (usages.length === 0) return;
8946
- if (effectHasCleanupReturn(callback, usages)) return;
8947
- const firstUsage = usages[0];
9342
+ const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
9343
+ if (!firstUsage) return;
8948
9344
  const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
9345
+ const hookName = getCalleeName$2(node) ?? "effect";
8949
9346
  context.report({
8950
9347
  node,
8951
- message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
9348
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8952
9349
  });
8953
9350
  },
8954
9351
  FunctionDeclaration(node) {
8955
9352
  if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8956
9353
  },
8957
9354
  ArrowFunctionExpression(node) {
8958
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9355
+ if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
8959
9356
  },
8960
9357
  FunctionExpression(node) {
8961
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9358
+ if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
8962
9359
  }
8963
9360
  };
8964
9361
  }
@@ -9572,13 +9969,18 @@ const unwrapExpression$3 = (node) => {
9572
9969
  return current;
9573
9970
  };
9574
9971
  /**
9575
- * Get the hook name from a call expression's callee, regardless of
9576
- * whether the hook is called as `useFoo()` (Identifier) or
9577
- * `React.useFoo()` (MemberExpression).
9972
+ * Get the hook name from a direct, wrapped, namespaced, or immutable
9973
+ * React import alias call.
9578
9974
  */
9579
- const getHookName = (callee) => {
9580
- if (isNodeOfType(callee, "Identifier")) return callee.name;
9581
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9975
+ const getHookName = (callee, scopes) => {
9976
+ const strippedCallee = unwrapExpression$3(callee);
9977
+ if (isNodeOfType(strippedCallee, "Identifier")) {
9978
+ const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
9979
+ const importDeclaration = resolvedSymbol?.declarationNode.parent;
9980
+ if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
9981
+ return strippedCallee.name;
9982
+ }
9983
+ if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
9582
9984
  return null;
9583
9985
  };
9584
9986
  const FUNCTION_SCOPE_KINDS = new Set([
@@ -9763,7 +10165,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
9763
10165
  * - primitive-literal local consts (the value never changes
9764
10166
  * between renders unless the literal does)
9765
10167
  */
9766
- const symbolHasStableHookOrigin = (symbol) => {
10168
+ const symbolHasStableHookOrigin = (symbol, scopes) => {
9767
10169
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
9768
10170
  let declarator = symbol.declarationNode;
9769
10171
  while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
@@ -9776,7 +10178,7 @@ const symbolHasStableHookOrigin = (symbol) => {
9776
10178
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
9777
10179
  }
9778
10180
  if (!isNodeOfType(initializer, "CallExpression")) return false;
9779
- const initializerHookName = getHookName(initializer.callee);
10181
+ const initializerHookName = getHookName(initializer.callee, scopes);
9780
10182
  if (!initializerHookName) return false;
9781
10183
  if (initializerHookName === "useRef") return true;
9782
10184
  if (initializerHookName === "useEffectEvent") return true;
@@ -9811,7 +10213,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
9811
10213
  if (!declarator.init) return false;
9812
10214
  const initializer = unwrapExpression$3(declarator.init);
9813
10215
  if (!isNodeOfType(initializer, "CallExpression")) return false;
9814
- const initializerHookName = getHookName(initializer.callee);
10216
+ const initializerHookName = getHookName(initializer.callee, scopes);
9815
10217
  if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
9816
10218
  const depsArgument = initializer.arguments[1];
9817
10219
  if (!depsArgument || !isAstNode(depsArgument)) return false;
@@ -9841,7 +10243,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
9841
10243
  }
9842
10244
  return null;
9843
10245
  };
9844
- const isStableRefContainerCapture = (symbol, depKey) => {
10246
+ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
9845
10247
  if (symbol.kind !== "const") return false;
9846
10248
  if (!depKey.startsWith(`${symbol.name}.`)) return false;
9847
10249
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
@@ -9855,7 +10257,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
9855
10257
  if (!propertyValue) return false;
9856
10258
  currentValue = propertyValue;
9857
10259
  }
9858
- return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
10260
+ return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
9859
10261
  };
9860
10262
  const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9861
10263
  if (visitedSymbolIds.has(symbol.id)) return true;
@@ -9873,7 +10275,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9873
10275
  }
9874
10276
  return true;
9875
10277
  };
9876
- const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
10278
+ const symbolHasStableImportedAlias = (symbol, scopes) => {
10279
+ if (symbol.kind !== "const") return false;
10280
+ if (symbol.references.some((reference) => reference.flag !== "read")) return false;
10281
+ const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
10282
+ return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
10283
+ };
10284
+ const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
9877
10285
  //#endregion
9878
10286
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9879
10287
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -10061,10 +10469,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
10061
10469
  }
10062
10470
  const depKey = computeDepKey(reference);
10063
10471
  if (!depKey) continue;
10064
- if (isStableRefContainerCapture(symbol, depKey)) {
10472
+ if (isStableRefContainerCapture(symbol, depKey, scopes)) {
10065
10473
  stableCapturedNames.add(depKey);
10066
10474
  continue;
10067
10475
  }
10476
+ if (depKey === symbol.name) {
10477
+ const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
10478
+ if (identitySourceKeys) {
10479
+ if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
10480
+ for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
10481
+ continue;
10482
+ }
10483
+ }
10068
10484
  keys.add(depKey);
10069
10485
  }
10070
10486
  return {
@@ -10093,12 +10509,60 @@ const hasComputedMemberExpression = (node) => {
10093
10509
  if (stripped.computed) return true;
10094
10510
  return hasComputedMemberExpression(stripped.object);
10095
10511
  };
10512
+ const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
10513
+ const identitySourceKeys = /* @__PURE__ */ new Set();
10514
+ for (const expression of expressions) {
10515
+ const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
10516
+ if (!expressionSourceKeys) return null;
10517
+ for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
10518
+ }
10519
+ return identitySourceKeys;
10520
+ };
10521
+ const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
10522
+ const stripped = unwrapExpression$3(expression);
10523
+ if (isNodeOfType(stripped, "Literal") && (stripped.value === null || typeof stripped.value === "string" || typeof stripped.value === "number" || typeof stripped.value === "boolean") || isNodeOfType(stripped, "TemplateLiteral") && getStaticTemplateLiteralValue(stripped) !== null) return /* @__PURE__ */ new Set();
10524
+ if (isNodeOfType(stripped, "Identifier")) {
10525
+ const sourceSymbol = scopes.symbolFor(stripped);
10526
+ if (!sourceSymbol) return null;
10527
+ if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
10528
+ if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
10529
+ if (visitedSymbolIds.has(sourceSymbol.id)) return null;
10530
+ visitedSymbolIds.add(sourceSymbol.id);
10531
+ const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
10532
+ visitedSymbolIds.delete(sourceSymbol.id);
10533
+ if (sourceKeys) return sourceKeys;
10534
+ }
10535
+ return new Set([sourceSymbol.name]);
10536
+ }
10537
+ if (isNodeOfType(stripped, "MemberExpression")) {
10538
+ if (hasComputedMemberExpression(stripped)) return null;
10539
+ const sourceKey = stringifyMemberChain(stripped);
10540
+ const rootIdentifier = getMemberRootIdentifier(stripped);
10541
+ const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
10542
+ if (!sourceKey || !rootSymbol) return null;
10543
+ if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
10544
+ if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
10545
+ if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
10546
+ return new Set([sourceKey]);
10547
+ }
10548
+ if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
10549
+ if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
10550
+ stripped.test,
10551
+ stripped.consequent,
10552
+ stripped.alternate
10553
+ ], scopes, visitedSymbolIds);
10554
+ return null;
10555
+ };
10556
+ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
10557
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
10558
+ return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
10559
+ };
10096
10560
  const isUseCallbackResultDep = (node, scopes) => {
10097
10561
  const rootSymbol = getRootSymbol(node, scopes);
10098
10562
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
10099
- return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
10563
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
10100
10564
  };
10101
- const isExtraEffectDepAllowed = (node, scopes) => {
10565
+ const isExtraReactiveDepAllowed = (node, scopes) => {
10102
10566
  const rootIdentifier = getMemberRootIdentifier(node);
10103
10567
  if (!rootIdentifier) return false;
10104
10568
  const symbol = scopes.symbolFor(rootIdentifier);
@@ -10126,12 +10590,78 @@ const isUnstableInitializer = (node) => {
10126
10590
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
10127
10591
  return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
10128
10592
  };
10593
+ const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
10594
+ const candidate = unwrapExpression$3(node);
10595
+ if (isUnstableInitializer(candidate)) return true;
10596
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
10597
+ if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
10598
+ if (!isNodeOfType(candidate, "Identifier")) return false;
10599
+ const symbol = scopes.symbolFor(candidate);
10600
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
10601
+ if (symbol.kind === "let" || symbol.kind === "var") return true;
10602
+ if (symbol.kind !== "const" || !symbol.initializer) return false;
10603
+ visitedSymbolIds.add(symbol.id);
10604
+ return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
10605
+ };
10606
+ const isExtraDepAllowedForHook = (hookName, node, scopes) => {
10607
+ if (!isExtraReactiveDepAllowed(node, scopes)) return false;
10608
+ if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
10609
+ if (hookName !== "useMemo") return false;
10610
+ const rootSymbol = getRootSymbol(node, scopes);
10611
+ return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
10612
+ };
10129
10613
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
10130
10614
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
10131
- const isStableSetterLikeSymbol = (symbol) => {
10132
- if (!symbolHasStableHookOrigin(symbol)) return false;
10615
+ const isStableSetterLikeSymbol = (symbol, scopes) => {
10616
+ if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
10133
10617
  return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
10134
10618
  };
10619
+ const isConvergingFunctionalUpdater = (node, scopes) => {
10620
+ const updater = unwrapExpression$3(node);
10621
+ if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
10622
+ const previousValueParameter = updater.params?.[0];
10623
+ if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
10624
+ let returnedExpression = updater.body;
10625
+ if (isNodeOfType(updater.body, "BlockStatement")) {
10626
+ returnedExpression = null;
10627
+ for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
10628
+ returnedExpression = statement.argument;
10629
+ break;
10630
+ }
10631
+ }
10632
+ const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
10633
+ if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
10634
+ const test = unwrapExpression$3(conditional.test);
10635
+ if (!isNodeOfType(test, "BinaryExpression") || ![
10636
+ "===",
10637
+ "!==",
10638
+ "==",
10639
+ "!="
10640
+ ].includes(test.operator)) return false;
10641
+ const isPreviousValue = (expression) => {
10642
+ const candidate = unwrapExpression$3(expression);
10643
+ return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
10644
+ };
10645
+ let comparedValue = null;
10646
+ if (isPreviousValue(test.left)) comparedValue = test.right;
10647
+ else if (isPreviousValue(test.right)) comparedValue = test.left;
10648
+ if (!comparedValue) return false;
10649
+ if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
10650
+ const isSameComparedValue = (expression) => {
10651
+ const candidate = unwrapExpression$3(expression);
10652
+ const compared = unwrapExpression$3(comparedValue);
10653
+ if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
10654
+ if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
10655
+ return false;
10656
+ };
10657
+ return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
10658
+ };
10659
+ const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
10660
+ const parent = identifier.parent;
10661
+ if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
10662
+ const writtenValue = parent.arguments?.[0];
10663
+ return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
10664
+ };
10135
10665
  const findStableSetterReference = (node, scopes) => {
10136
10666
  let setterName = null;
10137
10667
  const visit = (current) => {
@@ -10139,7 +10669,7 @@ const findStableSetterReference = (node, scopes) => {
10139
10669
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
10140
10670
  if (isNodeOfType(current, "Identifier")) {
10141
10671
  const symbol = scopes.referenceFor(current)?.resolvedSymbol;
10142
- if (symbol && isStableSetterLikeSymbol(symbol)) {
10672
+ if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
10143
10673
  setterName = symbol.name;
10144
10674
  return;
10145
10675
  }
@@ -10255,11 +10785,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
10255
10785
  }
10256
10786
  return false;
10257
10787
  };
10258
- const isSeededDataRefSymbol = (refSymbol) => {
10788
+ const isSeededDataRefSymbol = (refSymbol, scopes) => {
10259
10789
  if (!refSymbol) return false;
10260
10790
  const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
10261
10791
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
10262
- if (getHookName(initializer.callee) !== "useRef") return false;
10792
+ if (getHookName(initializer.callee, scopes) !== "useRef") return false;
10263
10793
  const firstArgument = initializer.arguments[0];
10264
10794
  if (!firstArgument || !isAstNode(firstArgument)) return false;
10265
10795
  const strippedArgument = unwrapExpression$3(firstArgument);
@@ -10299,7 +10829,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
10299
10829
  const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
10300
10830
  return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
10301
10831
  };
10302
- const hasMemberCallForRoot = (node, rootName) => {
10832
+ const hasMemberCallForRoot = (node, rootName, scopes) => {
10833
+ const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
10834
+ if (!rootSymbol) return false;
10303
10835
  let didFindMemberCall = false;
10304
10836
  const visit = (current) => {
10305
10837
  if (didFindMemberCall) return;
@@ -10315,7 +10847,7 @@ const hasMemberCallForRoot = (node, rootName) => {
10315
10847
  }
10316
10848
  chainObject = unwrapExpression$3(chainObject.object);
10317
10849
  }
10318
- if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
10850
+ if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
10319
10851
  didFindMemberCall = true;
10320
10852
  return;
10321
10853
  }
@@ -10333,9 +10865,11 @@ const hasMemberCallForRoot = (node, rootName) => {
10333
10865
  visit(node);
10334
10866
  return didFindMemberCall;
10335
10867
  };
10336
- const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
10337
- if ([...captureKeys].filter((captureKey) => captureKey.startsWith("props.")).length < 2 || declaredKeys.has("props")) return;
10338
- if (hasMemberCallForRoot(callback, "props")) captureKeys.add("props");
10868
+ const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
10869
+ const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
10870
+ if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
10871
+ if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
10872
+ if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
10339
10873
  };
10340
10874
  const exhaustiveDeps = defineRule({
10341
10875
  id: "exhaustive-deps",
@@ -10390,7 +10924,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10390
10924
  };
10391
10925
  const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
10392
10926
  return { CallExpression(node) {
10393
- const hookName = getHookName(node.callee);
10927
+ const hookName = getHookName(node.callee, context.scopes);
10394
10928
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
10395
10929
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10396
10930
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
@@ -10447,7 +10981,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10447
10981
  if (outerAssignments.length > 0) return;
10448
10982
  const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
10449
10983
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
10450
- if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
10984
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
10451
10985
  node: callbackToAnalyze,
10452
10986
  message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
10453
10987
  });
@@ -10546,7 +11080,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10546
11080
  const fullChain = stringifyMemberChain(stripped);
10547
11081
  if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
10548
11082
  const refSymbol = context.scopes.symbolFor(stripped.object);
10549
- if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
11083
+ if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
10550
11084
  if (!didReportRefCurrentDep) {
10551
11085
  context.report({
10552
11086
  node: elementNode,
@@ -10584,7 +11118,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10584
11118
  declaredKeys.add(key);
10585
11119
  declaredKeyToReportNode.set(key, elementNode);
10586
11120
  }
10587
- addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
11121
+ addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
10588
11122
  const missingCaptureKeys = [];
10589
11123
  for (const captureKey of captureKeys) {
10590
11124
  let isCoveredByDeclared = false;
@@ -10670,7 +11204,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10670
11204
  if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
10671
11205
  if (outerFunctionCapturedNames.has(rootName)) continue;
10672
11206
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
10673
- if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
11207
+ if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
10674
11208
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
10675
11209
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
10676
11210
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
@@ -12342,20 +12876,6 @@ const collectHandlerReferencedNames = (root) => {
12342
12876
  return names;
12343
12877
  };
12344
12878
  //#endregion
12345
- //#region src/plugin/utils/get-function-binding-name.ts
12346
- const getFunctionBindingIdentifier = (functionNode) => {
12347
- if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
12348
- const parent = functionNode.parent;
12349
- if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
12350
- if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
12351
- if (isNodeOfType(parent, "CallExpression")) {
12352
- const callParent = parent.parent;
12353
- if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
12354
- }
12355
- return null;
12356
- };
12357
- const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
12358
- //#endregion
12359
12879
  //#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
12360
12880
  const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
12361
12881
  const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -13042,7 +13562,7 @@ const jsCacheStorage = defineRule({
13042
13562
  CallExpression(node) {
13043
13563
  if (!isMemberProperty(node.callee, "getItem")) return;
13044
13564
  const receiver = stripParenExpression(node.callee.object);
13045
- if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
13565
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS.has(receiver.name)) return;
13046
13566
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
13047
13567
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
13048
13568
  const storageKey = String(node.arguments[0].value);
@@ -13309,6 +13829,11 @@ const jsEarlyExit = defineRule({
13309
13829
  });
13310
13830
  //#endregion
13311
13831
  //#region src/plugin/rules/js-performance/js-flatmap-filter.ts
13832
+ const BOUNDED_PIPELINE_SOURCE_METHOD_NAMES = new Set(["slice", "split"]);
13833
+ const isBoundedPipelineSource = (node) => {
13834
+ const receiver = stripParenExpression(node);
13835
+ return isNodeOfType(receiver, "CallExpression") && isNodeOfType(receiver.callee, "MemberExpression") && isNodeOfType(receiver.callee.property, "Identifier") && BOUNDED_PIPELINE_SOURCE_METHOD_NAMES.has(receiver.callee.property.name);
13836
+ };
13312
13837
  const jsFlatmapFilter = defineRule({
13313
13838
  id: "js-flatmap-filter",
13314
13839
  title: ".map().filter(Boolean) loops twice",
@@ -13326,6 +13851,7 @@ const jsFlatmapFilter = defineRule({
13326
13851
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
13327
13852
  if (innerCall.callee.property.name !== "map") return;
13328
13853
  const receiver = stripParenExpression(innerCall.callee.object);
13854
+ if (receiver && isBoundedPipelineSource(receiver)) return;
13329
13855
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
13330
13856
  const elements = receiver.elements ?? [];
13331
13857
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13500,7 +14026,7 @@ const jsHoistIntl = defineRule({
13500
14026
  });
13501
14027
  //#endregion
13502
14028
  //#region src/plugin/utils/create-loop-aware-visitors.ts
13503
- const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
14029
+ const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
13504
14030
  "map",
13505
14031
  "flatMap",
13506
14032
  "forEach",
@@ -13519,7 +14045,7 @@ const isIteratorCallback = (node) => {
13519
14045
  const parent = node.parent;
13520
14046
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
13521
14047
  if (!parent.arguments.includes(node)) return false;
13522
- return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
14048
+ return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
13523
14049
  };
13524
14050
  const createLoopAwareVisitors = (innerVisitors, options = {}) => {
13525
14051
  let loopDepth = 0;
@@ -13748,7 +14274,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
13748
14274
  });
13749
14275
  return indexedArrayObject;
13750
14276
  };
13751
- const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
14277
+ const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
13752
14278
  const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
13753
14279
  const LENGTH_MISMATCH_OPERATORS = new Set([
13754
14280
  "!==",
@@ -13760,17 +14286,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
13760
14286
  ]);
13761
14287
  const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
13762
14288
  const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
13763
- const binaryGuard = unwrapChainExpression$2(candidate);
14289
+ const binaryGuard = unwrapChainExpression$3(candidate);
13764
14290
  if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
13765
14291
  if (!operators.has(binaryGuard.operator)) return false;
13766
- const leftSide = unwrapChainExpression$2(binaryGuard.left);
13767
- const rightSide = unwrapChainExpression$2(binaryGuard.right);
14292
+ const leftSide = unwrapChainExpression$3(binaryGuard.left);
14293
+ const rightSide = unwrapChainExpression$3(binaryGuard.right);
13768
14294
  if (!isMemberProperty(leftSide, "length")) return false;
13769
14295
  if (!isMemberProperty(rightSide, "length")) return false;
13770
- const leftLengthObject = unwrapChainExpression$2(leftSide.object);
13771
- const rightLengthObject = unwrapChainExpression$2(rightSide.object);
13772
- const normalizedReceiver = unwrapChainExpression$2(receiverArray);
13773
- const normalizedIndexed = unwrapChainExpression$2(indexedArray);
14296
+ const leftLengthObject = unwrapChainExpression$3(leftSide.object);
14297
+ const rightLengthObject = unwrapChainExpression$3(rightSide.object);
14298
+ const normalizedReceiver = unwrapChainExpression$3(receiverArray);
14299
+ const normalizedIndexed = unwrapChainExpression$3(indexedArray);
13774
14300
  const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
13775
14301
  const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
13776
14302
  return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
@@ -13857,18 +14383,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
13857
14383
  "toReversed"
13858
14384
  ]);
13859
14385
  const peelLengthPreservingDerivation = (expression) => {
13860
- let current = unwrapChainExpression$2(expression);
14386
+ let current = unwrapChainExpression$3(expression);
13861
14387
  for (;;) {
13862
14388
  if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
13863
- current = unwrapChainExpression$2(current.elements[0].argument);
14389
+ current = unwrapChainExpression$3(current.elements[0].argument);
13864
14390
  continue;
13865
14391
  }
13866
14392
  if (isNodeOfType(current, "CallExpression")) {
13867
- const callee = unwrapChainExpression$2(current.callee);
14393
+ const callee = unwrapChainExpression$3(current.callee);
13868
14394
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
13869
- const calleeObject = unwrapChainExpression$2(callee.object);
14395
+ const calleeObject = unwrapChainExpression$3(callee.object);
13870
14396
  if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
13871
- current = unwrapChainExpression$2(current.arguments[0]);
14397
+ current = unwrapChainExpression$3(current.arguments[0]);
13872
14398
  continue;
13873
14399
  }
13874
14400
  if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
@@ -13913,7 +14439,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
13913
14439
  const initializer = findConstInitializer(current.name, scopeNode);
13914
14440
  if (!initializer) return current;
13915
14441
  const peeledInitializer = peelLengthPreservingDerivation(initializer);
13916
- if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
14442
+ if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
13917
14443
  current = peeledInitializer;
13918
14444
  }
13919
14445
  return current;
@@ -19643,54 +20169,6 @@ const nextjsNoAElement = defineRule({
19643
20169
  } })
19644
20170
  });
19645
20171
  //#endregion
19646
- //#region src/plugin/utils/collect-effect-invoked-functions.ts
19647
- const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
19648
- "then",
19649
- "catch",
19650
- "finally"
19651
- ]);
19652
- const collectEffectInvokedFunctions = (effectCallback) => {
19653
- const invokedFunctions = new Set([effectCallback]);
19654
- const localFunctionBindings = /* @__PURE__ */ new Map();
19655
- const calledBindingNames = /* @__PURE__ */ new Set();
19656
- const pendingFunctions = [effectCallback];
19657
- const enqueue = (candidate) => {
19658
- const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
19659
- if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
19660
- invokedFunctions.add(strippedCandidate);
19661
- pendingFunctions.push(strippedCandidate);
19662
- };
19663
- const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
19664
- while (pendingFunctions.length > 0) {
19665
- const currentFunction = pendingFunctions.pop();
19666
- if (!currentFunction) break;
19667
- walkAst(currentFunction, (child) => {
19668
- if (child !== currentFunction && isFunctionLike$1(child)) {
19669
- if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
19670
- return false;
19671
- }
19672
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
19673
- const initializer = child.init ? stripParenExpression(child.init) : null;
19674
- if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
19675
- return;
19676
- }
19677
- if (!isNodeOfType(child, "CallExpression")) return;
19678
- const callee = stripParenExpression(child.callee);
19679
- if (isFunctionLike$1(callee)) {
19680
- enqueue(callee);
19681
- return;
19682
- }
19683
- if (isNodeOfType(callee, "Identifier")) {
19684
- calledBindingNames.add(callee.name);
19685
- return;
19686
- }
19687
- if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
19688
- });
19689
- for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
19690
- }
19691
- return invokedFunctions;
19692
- };
19693
- //#endregion
19694
20172
  //#region src/plugin/utils/contains-fetch-call.ts
19695
20173
  const isFetchCall$1 = (node) => {
19696
20174
  if (!isNodeOfType(node, "CallExpression")) return false;
@@ -20674,6 +21152,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20674
21152
  continue;
20675
21153
  }
20676
21154
  if (isNodeOfType(statement, "ExportNamedDeclaration")) {
21155
+ if (statement.exportKind === "type") continue;
20677
21156
  const declaration = statement.declaration;
20678
21157
  if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
20679
21158
  recordVariableDeclaration(declaration);
@@ -20688,6 +21167,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20688
21167
  }
20689
21168
  for (const specifier of statement.specifiers ?? []) {
20690
21169
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21170
+ if (specifier.exportKind === "type") continue;
20691
21171
  const local = specifier.local;
20692
21172
  const exported = specifier.exported;
20693
21173
  if (!isNodeOfType(local, "Identifier")) continue;
@@ -20736,25 +21216,37 @@ const resolveImportedExportName = (importSpecifier) => {
20736
21216
  if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
20737
21217
  return null;
20738
21218
  };
20739
- const findReExportSourcesForName = (programRoot, exportedName) => {
21219
+ const findReExportTargetsForName = (programRoot, exportedName) => {
20740
21220
  if (!isNodeOfType(programRoot, "Program")) return [];
20741
- const exportAllSources = [];
21221
+ const exportAllTargets = [];
20742
21222
  for (const statement of programRoot.body ?? []) {
20743
21223
  if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
21224
+ if (statement.exportKind === "type") continue;
20744
21225
  const sourceValue = statement.source.value;
20745
21226
  if (typeof sourceValue !== "string") continue;
20746
21227
  for (const specifier of statement.specifiers ?? []) {
20747
21228
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21229
+ if (specifier.exportKind === "type") continue;
20748
21230
  const exported = specifier.exported;
20749
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
21231
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
21232
+ const local = specifier.local;
21233
+ const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
21234
+ if (importedName) return [{
21235
+ importedName,
21236
+ source: sourceValue
21237
+ }];
20750
21238
  }
20751
21239
  }
20752
21240
  if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
21241
+ if (statement.exportKind === "type" || statement.exported) continue;
20753
21242
  const sourceValue = statement.source.value;
20754
- if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
21243
+ if (typeof sourceValue === "string") exportAllTargets.push({
21244
+ importedName: exportedName,
21245
+ source: sourceValue
21246
+ });
20755
21247
  }
20756
21248
  }
20757
- return exportAllSources;
21249
+ return exportAllTargets;
20758
21250
  };
20759
21251
  //#endregion
20760
21252
  //#region src/plugin/utils/attach-parent-references.ts
@@ -20843,139 +21335,6 @@ const parseSourceFile = (absoluteFilePath) => {
20843
21335
  return parsedProgram;
20844
21336
  };
20845
21337
  //#endregion
20846
- //#region src/plugin/utils/is-barrel-index-module.ts
20847
- const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
20848
- const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
20849
- const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
20850
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
20851
- const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
20852
- const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
20853
- const createNonBarrelInfo = () => ({
20854
- isBarrel: false,
20855
- exportsByName: /* @__PURE__ */ new Map(),
20856
- starExportSources: []
20857
- });
20858
- const addImportedBinding = (importedBindings, binding) => {
20859
- importedBindings.set(binding.localName, {
20860
- ...binding,
20861
- didExport: false
20862
- });
20863
- };
20864
- const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
20865
- for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
20866
- localName: specifier.exportedName,
20867
- importedName: specifier.localName,
20868
- source,
20869
- isTypeOnly: specifier.isTypeOnly
20870
- });
20871
- };
20872
- const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
20873
- const trimmedImportClause = importClause.trim();
20874
- const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
20875
- if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
20876
- localName: namespaceMatch[1],
20877
- importedName: "*",
20878
- source,
20879
- isTypeOnly: declarationIsTypeOnly
20880
- });
20881
- const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
20882
- if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
20883
- const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
20884
- if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
20885
- localName: defaultImportName,
20886
- importedName: "default",
20887
- source,
20888
- isTypeOnly: declarationIsTypeOnly
20889
- });
20890
- };
20891
- const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
20892
- let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
20893
- collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
20894
- return "";
20895
- });
20896
- withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
20897
- const isTypeOnly = Boolean(typeKeyword);
20898
- if (namespaceExportName) {
20899
- exportsByName.set(namespaceExportName, {
20900
- exportedName: namespaceExportName,
20901
- importedName: "*",
20902
- source,
20903
- isTypeOnly
20904
- });
20905
- return "";
20906
- }
20907
- if (specifiersText) {
20908
- for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
20909
- exportedName: specifier.exportedName,
20910
- importedName: specifier.localName,
20911
- source,
20912
- isTypeOnly: specifier.isTypeOnly
20913
- });
20914
- return "";
20915
- }
20916
- starExportSources.push(source);
20917
- return "";
20918
- });
20919
- withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
20920
- for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
20921
- const importedBinding = importedBindings.get(specifier.localName);
20922
- if (!importedBinding) return _match;
20923
- importedBinding.didExport = true;
20924
- exportsByName.set(specifier.exportedName, {
20925
- exportedName: specifier.exportedName,
20926
- importedName: importedBinding.importedName,
20927
- source: importedBinding.source,
20928
- isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
20929
- });
20930
- }
20931
- return "";
20932
- });
20933
- return withoutKnownDeclarations;
20934
- };
20935
- const hasUnexportedRuntimeImport = (importedBindings) => {
20936
- for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
20937
- return false;
20938
- };
20939
- const classifyBarrelModule = (sourceText) => {
20940
- const strippedSource = stripJsComments(sourceText).trim();
20941
- if (!strippedSource) return createNonBarrelInfo();
20942
- const importedBindings = /* @__PURE__ */ new Map();
20943
- const exportsByName = /* @__PURE__ */ new Map();
20944
- const starExportSources = [];
20945
- if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
20946
- return {
20947
- isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
20948
- exportsByName,
20949
- starExportSources
20950
- };
20951
- };
20952
- const getBarrelIndexModuleInfo = (filePath) => {
20953
- if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
20954
- recordContentProbe(filePath);
20955
- let fileStat;
20956
- try {
20957
- fileStat = fs.statSync(filePath);
20958
- } catch {
20959
- fileStat = null;
20960
- }
20961
- const cachedResult = barrelIndexModuleInfoCache.get(filePath);
20962
- if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
20963
- if (fileStat === null) return createNonBarrelInfo();
20964
- let moduleInfo = createNonBarrelInfo();
20965
- try {
20966
- moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
20967
- } catch {
20968
- moduleInfo = createNonBarrelInfo();
20969
- }
20970
- barrelIndexModuleInfoCache.set(filePath, {
20971
- mtimeMs: fileStat.mtimeMs,
20972
- size: fileStat.size,
20973
- moduleInfo
20974
- });
20975
- return moduleInfo;
20976
- };
20977
- const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
20978
- //#endregion
20979
21338
  //#region src/plugin/utils/resolve-relative-import-path.ts
20980
21339
  const MODULE_FILE_EXTENSIONS = [
20981
21340
  ".ts",
@@ -21092,35 +21451,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
21092
21451
  };
21093
21452
  const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
21094
21453
  //#endregion
21095
- //#region src/plugin/utils/resolve-barrel-export-file-path.ts
21096
- const getUniqueFilePath = (filePaths) => {
21097
- const uniqueFilePaths = new Set(filePaths);
21098
- if (uniqueFilePaths.size !== 1) return null;
21099
- const [filePath] = uniqueFilePaths;
21100
- return filePath ?? null;
21101
- };
21102
- const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
21103
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
21104
- if (!resolvedTargetPath) return null;
21105
- const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
21106
- if (nestedTargetPath) return nestedTargetPath;
21107
- return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
21108
- };
21109
- const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
21110
- if (visitedFilePaths.has(barrelFilePath)) return null;
21111
- visitedFilePaths.add(barrelFilePath);
21112
- const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
21113
- if (!moduleInfo.isBarrel) return null;
21114
- const target = moduleInfo.exportsByName.get(exportedName);
21115
- if (target) {
21116
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
21117
- if (!resolvedTargetPath) return null;
21118
- return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
21119
- }
21120
- if (exportedName === "default") return null;
21121
- return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
21122
- };
21123
- //#endregion
21124
21454
  //#region src/plugin/utils/resolve-tsconfig-alias.ts
21125
21455
  const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
21126
21456
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
@@ -21299,25 +21629,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
21299
21629
  };
21300
21630
  //#endregion
21301
21631
  //#region src/plugin/utils/resolve-module-path.ts
21302
- const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
21632
+ const resolveModulePath = (fromFilename, source) => {
21633
+ if (path.isAbsolute(source)) return null;
21634
+ return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
21635
+ };
21303
21636
  //#endregion
21304
21637
  //#region src/plugin/utils/resolve-cross-file-function-export.ts
21305
21638
  const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
21306
21639
  if (visitedFilePaths.size >= 4) return null;
21307
21640
  if (visitedFilePaths.has(filePath)) return null;
21308
21641
  visitedFilePaths.add(filePath);
21309
- const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
21310
- const programRoot = parseSourceFile(actualFilePath);
21642
+ const programRoot = parseSourceFile(filePath);
21311
21643
  if (!programRoot) return null;
21312
21644
  const exported = findExportedFunctionBody(programRoot, exportedName);
21313
21645
  if (exported) return exported;
21314
- for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
21315
- const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
21646
+ const resolvedCandidates = /* @__PURE__ */ new Set();
21647
+ for (const target of findReExportTargetsForName(programRoot, exportedName)) {
21648
+ const nextFilePath = resolveModulePath(filePath, target.source);
21316
21649
  if (!nextFilePath) continue;
21317
- const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
21318
- if (resolved) return resolved;
21650
+ const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
21651
+ if (resolved) resolvedCandidates.add(resolved);
21319
21652
  }
21320
- return null;
21653
+ if (resolvedCandidates.size !== 1) return null;
21654
+ return resolvedCandidates.values().next().value ?? null;
21321
21655
  };
21322
21656
  const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
21323
21657
  const resolvedFilePath = resolveModulePath(fromFilename, source);
@@ -22102,6 +22436,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22102
22436
  const importDeclaration = declarationNode.parent;
22103
22437
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22104
22438
  }));
22439
+ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
22440
+ if (def.type !== "ImportBinding") return false;
22441
+ const declarationNode = def.node;
22442
+ if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
22443
+ const importDeclaration = declarationNode.parent;
22444
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22445
+ }));
22446
+ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
22447
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
22448
+ const callee = stripParenExpression(declarator.init.callee);
22449
+ if (isNodeOfType(callee, "Identifier")) {
22450
+ const reference = getRef(analysis, callee);
22451
+ if (!reference?.resolved) return callee.name === hookName;
22452
+ return isReactNamedImportReference(reference, hookName);
22453
+ }
22454
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
22455
+ const namespaceReference = getRef(analysis, callee.object);
22456
+ if (!namespaceReference?.resolved) return callee.object.name === "React";
22457
+ return isReactNamespaceImportReference(namespaceReference);
22458
+ };
22105
22459
  const isHookCallee$1 = (analysis, node, hookName) => {
22106
22460
  if (!node) return false;
22107
22461
  if (isNodeOfType(node, "Identifier")) {
@@ -22500,6 +22854,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22500
22854
  "parseInt"
22501
22855
  ]);
22502
22856
  const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22857
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22858
+ "isRawJSON",
22859
+ "parse",
22860
+ "rawJSON",
22861
+ "stringify"
22862
+ ])], ["Math", new Set([
22863
+ "abs",
22864
+ "acos",
22865
+ "acosh",
22866
+ "asin",
22867
+ "asinh",
22868
+ "atan",
22869
+ "atan2",
22870
+ "atanh",
22871
+ "cbrt",
22872
+ "ceil",
22873
+ "clz32",
22874
+ "cos",
22875
+ "cosh",
22876
+ "exp",
22877
+ "floor",
22878
+ "fround",
22879
+ "hypot",
22880
+ "imul",
22881
+ "log",
22882
+ "log10",
22883
+ "log1p",
22884
+ "log2",
22885
+ "max",
22886
+ "min",
22887
+ "pow",
22888
+ "round",
22889
+ "sign",
22890
+ "sin",
22891
+ "sinh",
22892
+ "sqrt",
22893
+ "tan",
22894
+ "tanh",
22895
+ "trunc"
22896
+ ])]]);
22503
22897
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22504
22898
  "concat",
22505
22899
  "filter",
@@ -22546,6 +22940,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
22546
22940
  }
22547
22941
  return parameter;
22548
22942
  };
22943
+ const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
22944
+ const isModuleFunction = (functionNode) => {
22945
+ let ancestor = functionNode.parent;
22946
+ while (ancestor) {
22947
+ if (isFunctionLike$1(ancestor)) return false;
22948
+ if (isNodeOfType(ancestor, "Program")) return true;
22949
+ ancestor = ancestor.parent;
22950
+ }
22951
+ return false;
22952
+ };
22953
+ const getFunctionBindingNames = (functionNode) => {
22954
+ const names = /* @__PURE__ */ new Set();
22955
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
22956
+ const parent = functionNode.parent;
22957
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
22958
+ return names;
22959
+ };
22960
+ const collectModuleBindingNames = (functionNode) => {
22961
+ let program = functionNode.parent;
22962
+ while (program && !isNodeOfType(program, "Program")) program = program.parent;
22963
+ const bindingNames = /* @__PURE__ */ new Set();
22964
+ if (!program || !isNodeOfType(program, "Program")) return bindingNames;
22965
+ for (const statement of program.body ?? []) {
22966
+ if (isNodeOfType(statement, "ImportDeclaration")) {
22967
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
22968
+ continue;
22969
+ }
22970
+ const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
22971
+ if (isNodeOfType(declaration, "VariableDeclaration")) {
22972
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
22973
+ continue;
22974
+ }
22975
+ if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
22976
+ }
22977
+ return bindingNames;
22978
+ };
22549
22979
  const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
22550
22980
  const substitutions = /* @__PURE__ */ new Map();
22551
22981
  const parameters = getFunctionParameters(functionNode);
@@ -22676,7 +23106,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
22676
23106
  for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
22677
23107
  return introducedBindings;
22678
23108
  };
22679
- const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
23109
+ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
22680
23110
  const effectFunction = getEffectFn(analysis, effectNode);
22681
23111
  if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
22682
23112
  const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
@@ -22685,7 +23115,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22685
23115
  invocation: null,
22686
23116
  isDeferred: false,
22687
23117
  introducedBindings: /* @__PURE__ */ new Set(),
22688
- substitutions: /* @__PURE__ */ new Map()
23118
+ substitutions: /* @__PURE__ */ new Map(),
23119
+ currentFilename
22689
23120
  };
22690
23121
  const frames = [rootFrame];
22691
23122
  walkAst(effectFunction, (child) => {
@@ -22706,7 +23137,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22706
23137
  invocation: child,
22707
23138
  isDeferred,
22708
23139
  introducedBindings,
22709
- substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
23140
+ substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
23141
+ currentFilename
22710
23142
  });
22711
23143
  };
22712
23144
  if (isFunctionLike$1(callee)) {
@@ -22759,6 +23191,174 @@ const isOpaqueHookCall = (callExpression) => {
22759
23191
  const calleeName = getCallCalleeName$1(callExpression);
22760
23192
  return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
22761
23193
  };
23194
+ const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
23195
+ let canContinue = true;
23196
+ for (const statement of statements) {
23197
+ if (!canContinue) {
23198
+ if (!isNodeOfType(statement, "EmptyStatement")) return {
23199
+ canContinue: false,
23200
+ isValid: false
23201
+ };
23202
+ continue;
23203
+ }
23204
+ if (isNodeOfType(statement, "EmptyStatement")) continue;
23205
+ if (isNodeOfType(statement, "ReturnStatement")) {
23206
+ if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
23207
+ canContinue: false,
23208
+ isValid: false
23209
+ };
23210
+ canContinue = false;
23211
+ continue;
23212
+ }
23213
+ if (isNodeOfType(statement, "BlockStatement")) {
23214
+ const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
23215
+ if (!blockSummary.isValid) return blockSummary;
23216
+ canContinue = blockSummary.canContinue;
23217
+ continue;
23218
+ }
23219
+ if (isNodeOfType(statement, "IfStatement")) {
23220
+ if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
23221
+ canContinue: false,
23222
+ isValid: false
23223
+ };
23224
+ const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
23225
+ if (!consequentSummary.isValid) return consequentSummary;
23226
+ const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
23227
+ canContinue: true,
23228
+ isValid: true
23229
+ };
23230
+ if (!alternateSummary.isValid) return alternateSummary;
23231
+ canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
23232
+ continue;
23233
+ }
23234
+ return {
23235
+ canContinue: false,
23236
+ isValid: false
23237
+ };
23238
+ }
23239
+ return {
23240
+ canContinue,
23241
+ isValid: true
23242
+ };
23243
+ };
23244
+ const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
23245
+ const node = stripParenExpression(expression);
23246
+ if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
23247
+ if (isNodeOfType(node, "Identifier")) {
23248
+ const parameterIndex = environment.parameterIndices.get(node.name);
23249
+ if (parameterIndex !== void 0) {
23250
+ if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
23251
+ return true;
23252
+ }
23253
+ return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
23254
+ }
23255
+ if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
23256
+ if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
23257
+ if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
23258
+ if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
23259
+ if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
23260
+ return analyzeHelperExpression(property.value, environment, usedParameterIndices);
23261
+ });
23262
+ if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
23263
+ if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23264
+ if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
23265
+ if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
23266
+ if (isNodeOfType(node, "MemberExpression")) {
23267
+ if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
23268
+ return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
23269
+ }
23270
+ if (isFunctionLike$1(node)) {
23271
+ if (isAsyncOrGeneratorFunction(node)) return false;
23272
+ const callbackParameterIndices = new Map(environment.parameterIndices);
23273
+ for (const parameter of getFunctionParameters(node)) {
23274
+ if (!isNodeOfType(parameter, "Identifier")) return false;
23275
+ callbackParameterIndices.set(parameter.name, null);
23276
+ }
23277
+ const callbackEnvironment = {
23278
+ parameterIndices: callbackParameterIndices,
23279
+ recursiveNames: environment.recursiveNames,
23280
+ shadowedGlobalNames: environment.shadowedGlobalNames
23281
+ };
23282
+ if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
23283
+ const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23284
+ return callbackSummary.isValid && !callbackSummary.canContinue;
23285
+ }
23286
+ if (isNodeOfType(node, "CallExpression")) {
23287
+ const callee = stripParenExpression(node.callee);
23288
+ const calleeRoot = getMemberRoot(callee);
23289
+ const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
23290
+ const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23291
+ const namespaceMemberName = getStaticMemberName(callee);
23292
+ const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23293
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23294
+ if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23295
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23296
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23297
+ }
23298
+ if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23299
+ return false;
23300
+ };
23301
+ const helperSummaryCache = /* @__PURE__ */ new WeakMap();
23302
+ const summarizeHelperReturn = (functionNode) => {
23303
+ if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
23304
+ if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
23305
+ if (isAsyncOrGeneratorFunction(functionNode)) {
23306
+ helperSummaryCache.set(functionNode, null);
23307
+ return null;
23308
+ }
23309
+ const parameterIndices = /* @__PURE__ */ new Map();
23310
+ const parameters = getFunctionParameters(functionNode);
23311
+ for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
23312
+ const parameter = parameters[parameterIndex];
23313
+ if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
23314
+ helperSummaryCache.set(functionNode, null);
23315
+ return null;
23316
+ }
23317
+ parameterIndices.set(parameter.name, parameterIndex);
23318
+ }
23319
+ const environment = {
23320
+ parameterIndices,
23321
+ recursiveNames: getFunctionBindingNames(functionNode),
23322
+ shadowedGlobalNames: collectModuleBindingNames(functionNode)
23323
+ };
23324
+ const usedParameterIndices = /* @__PURE__ */ new Set();
23325
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) {
23326
+ if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
23327
+ helperSummaryCache.set(functionNode, null);
23328
+ return null;
23329
+ }
23330
+ } else {
23331
+ const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
23332
+ if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
23333
+ helperSummaryCache.set(functionNode, null);
23334
+ return null;
23335
+ }
23336
+ }
23337
+ const summary = { usedParameterIndices };
23338
+ helperSummaryCache.set(functionNode, summary);
23339
+ return summary;
23340
+ };
23341
+ const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
23342
+ if (!isNodeOfType(callee, "Identifier")) return null;
23343
+ const reference = getRef(analysis, callee);
23344
+ if (!reference?.resolved) return null;
23345
+ const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
23346
+ if (importDefinition) {
23347
+ if (!currentFilename) return null;
23348
+ const specifier = importDefinition.node;
23349
+ if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
23350
+ const importDeclaration = specifier.parent;
23351
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
23352
+ if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
23353
+ const source = importDeclaration.source?.value;
23354
+ if (typeof source !== "string") return null;
23355
+ const exportedName = resolveImportedExportName(specifier);
23356
+ if (!exportedName) return null;
23357
+ return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
23358
+ }
23359
+ const callable = resolveWrappedCallable(analysis, callee);
23360
+ return callable && isModuleFunction(callable) ? callable : null;
23361
+ };
22762
23362
  const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
22763
23363
  const definitionNode = definition.node;
22764
23364
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
@@ -22844,38 +23444,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
22844
23444
  const calleeRoot = getMemberRoot(callee);
22845
23445
  const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
22846
23446
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
22847
- if (isPureMemberTransform) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
22848
- for (const argument of node.arguments ?? []) {
22849
- if (isFunctionLike$1(argument)) continue;
22850
- mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
22851
- }
22852
- if (isPureGlobalCall || isPureMemberTransform) return evidence;
22853
- if (isNodeOfType(callee, "MemberExpression")) {
22854
- evidence.hasUnknownSource = true;
23447
+ if (isPureGlobalCall || isPureMemberTransform) {
23448
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23449
+ for (const argument of node.arguments ?? []) {
23450
+ if (isFunctionLike$1(argument)) continue;
23451
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23452
+ }
22855
23453
  return evidence;
22856
23454
  }
22857
23455
  if (remainingCallFrames <= 0) {
22858
23456
  evidence.hasUnknownSource = true;
22859
23457
  return evidence;
22860
23458
  }
22861
- const callable = resolveWrappedCallable(analysis, callee);
22862
- if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
22863
- evidence.hasUnknownSource = true;
23459
+ const localHelperFunction = resolveWrappedCallable(analysis, callee);
23460
+ if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
23461
+ if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
23462
+ evidence.hasUnknownSource = true;
23463
+ return evidence;
23464
+ }
23465
+ const localHelperFrame = {
23466
+ functionNode: localHelperFunction,
23467
+ invocation: node,
23468
+ isDeferred: false,
23469
+ introducedBindings: /* @__PURE__ */ new Set(),
23470
+ substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
23471
+ currentFilename: frame.currentFilename
23472
+ };
23473
+ const returnedExpressions = getReturnedExpressions(localHelperFunction);
23474
+ if (returnedExpressions.length === 0) {
23475
+ evidence.hasUnknownSource = true;
23476
+ return evidence;
23477
+ }
23478
+ for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
22864
23479
  return evidence;
22865
23480
  }
22866
- const valueFrame = {
22867
- functionNode: callable,
22868
- invocation: node,
22869
- isDeferred: false,
22870
- introducedBindings: /* @__PURE__ */ new Set(),
22871
- substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
22872
- };
22873
- const returnedExpressions = getReturnedExpressions(callable);
22874
- if (returnedExpressions.length === 0) {
23481
+ const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
23482
+ const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
23483
+ if (!helperSummary) {
22875
23484
  evidence.hasUnknownSource = true;
22876
23485
  return evidence;
22877
23486
  }
22878
- for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23487
+ const argumentsForHelper = node.arguments ?? [];
23488
+ for (const parameterIndex of helperSummary.usedParameterIndices) {
23489
+ const argument = argumentsForHelper[parameterIndex];
23490
+ if (!argument) {
23491
+ evidence.hasUnknownSource = true;
23492
+ return evidence;
23493
+ }
23494
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
23495
+ }
22879
23496
  return evidence;
22880
23497
  }
22881
23498
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
@@ -22893,6 +23510,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
22893
23510
  }
22894
23511
  return evidence;
22895
23512
  };
23513
+ const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
23514
+ const evidence = collectValueEvidence(analysis, expression, {
23515
+ functionNode: componentFunction,
23516
+ invocation: null,
23517
+ isDeferred: false,
23518
+ introducedBindings: /* @__PURE__ */ new Set(),
23519
+ substitutions: /* @__PURE__ */ new Map(),
23520
+ currentFilename
23521
+ }, 1);
23522
+ return {
23523
+ sourceReferences: evidence.sourceReferences,
23524
+ isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
23525
+ };
23526
+ };
22896
23527
  const findStateSetterReference = (analysis, callExpression) => {
22897
23528
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
22898
23529
  const callee = stripParenExpression(callExpression.callee);
@@ -22955,8 +23586,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
22955
23586
  }
22956
23587
  return false;
22957
23588
  };
22958
- const collectEffectStateWriteFacts = (analysis, effectNode) => {
22959
- const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
23589
+ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
23590
+ const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
22960
23591
  if (frames.length === 0) return [];
22961
23592
  const effectHasCleanup = hasCleanup(analysis, effectNode);
22962
23593
  const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
@@ -22976,7 +23607,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
22976
23607
  invocation: callExpression,
22977
23608
  isDeferred: frame.isDeferred,
22978
23609
  introducedBindings: collectIntroducedBindings(analysis, writtenValue),
22979
- substitutions: /* @__PURE__ */ new Map()
23610
+ substitutions: /* @__PURE__ */ new Map(),
23611
+ currentFilename
22980
23612
  };
22981
23613
  valueEvidence = emptyEvidence();
22982
23614
  const returnedExpressions = getReturnedExpressions(writtenValue);
@@ -23025,7 +23657,7 @@ const noAdjustStateOnPropChange = defineRule({
23025
23657
  const dependencyReferences = getEffectDepsRefs(analysis, node);
23026
23658
  if (!dependencyReferences) return;
23027
23659
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
23028
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
23660
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
23029
23661
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
23030
23662
  context.report({
23031
23663
  node: fact.callExpression,
@@ -24286,6 +24918,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
24286
24918
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
24287
24919
  };
24288
24920
  //#endregion
24921
+ //#region src/plugin/utils/is-barrel-index-module.ts
24922
+ const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
24923
+ const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24924
+ const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24925
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24926
+ const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
24927
+ const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
24928
+ const createNonBarrelInfo = () => ({
24929
+ isBarrel: false,
24930
+ exportsByName: /* @__PURE__ */ new Map(),
24931
+ starExportSources: []
24932
+ });
24933
+ const addImportedBinding = (importedBindings, binding) => {
24934
+ importedBindings.set(binding.localName, {
24935
+ ...binding,
24936
+ didExport: false
24937
+ });
24938
+ };
24939
+ const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
24940
+ for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
24941
+ localName: specifier.exportedName,
24942
+ importedName: specifier.localName,
24943
+ source,
24944
+ isTypeOnly: specifier.isTypeOnly
24945
+ });
24946
+ };
24947
+ const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
24948
+ const trimmedImportClause = importClause.trim();
24949
+ const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
24950
+ if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
24951
+ localName: namespaceMatch[1],
24952
+ importedName: "*",
24953
+ source,
24954
+ isTypeOnly: declarationIsTypeOnly
24955
+ });
24956
+ const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
24957
+ if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
24958
+ const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
24959
+ if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
24960
+ localName: defaultImportName,
24961
+ importedName: "default",
24962
+ source,
24963
+ isTypeOnly: declarationIsTypeOnly
24964
+ });
24965
+ };
24966
+ const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
24967
+ let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
24968
+ collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
24969
+ return "";
24970
+ });
24971
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
24972
+ const isTypeOnly = Boolean(typeKeyword);
24973
+ if (namespaceExportName) {
24974
+ exportsByName.set(namespaceExportName, {
24975
+ exportedName: namespaceExportName,
24976
+ importedName: "*",
24977
+ source,
24978
+ isTypeOnly
24979
+ });
24980
+ return "";
24981
+ }
24982
+ if (specifiersText) {
24983
+ for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
24984
+ exportedName: specifier.exportedName,
24985
+ importedName: specifier.localName,
24986
+ source,
24987
+ isTypeOnly: specifier.isTypeOnly
24988
+ });
24989
+ return "";
24990
+ }
24991
+ starExportSources.push(source);
24992
+ return "";
24993
+ });
24994
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
24995
+ for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
24996
+ const importedBinding = importedBindings.get(specifier.localName);
24997
+ if (!importedBinding) return _match;
24998
+ importedBinding.didExport = true;
24999
+ exportsByName.set(specifier.exportedName, {
25000
+ exportedName: specifier.exportedName,
25001
+ importedName: importedBinding.importedName,
25002
+ source: importedBinding.source,
25003
+ isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
25004
+ });
25005
+ }
25006
+ return "";
25007
+ });
25008
+ return withoutKnownDeclarations;
25009
+ };
25010
+ const hasUnexportedRuntimeImport = (importedBindings) => {
25011
+ for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
25012
+ return false;
25013
+ };
25014
+ const classifyBarrelModule = (sourceText) => {
25015
+ const strippedSource = stripJsComments(sourceText).trim();
25016
+ if (!strippedSource) return createNonBarrelInfo();
25017
+ const importedBindings = /* @__PURE__ */ new Map();
25018
+ const exportsByName = /* @__PURE__ */ new Map();
25019
+ const starExportSources = [];
25020
+ if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
25021
+ return {
25022
+ isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
25023
+ exportsByName,
25024
+ starExportSources
25025
+ };
25026
+ };
25027
+ const getBarrelIndexModuleInfo = (filePath) => {
25028
+ if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
25029
+ recordContentProbe(filePath);
25030
+ let fileStat;
25031
+ try {
25032
+ fileStat = fs.statSync(filePath);
25033
+ } catch {
25034
+ fileStat = null;
25035
+ }
25036
+ const cachedResult = barrelIndexModuleInfoCache.get(filePath);
25037
+ if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
25038
+ if (fileStat === null) return createNonBarrelInfo();
25039
+ let moduleInfo = createNonBarrelInfo();
25040
+ try {
25041
+ moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
25042
+ } catch {
25043
+ moduleInfo = createNonBarrelInfo();
25044
+ }
25045
+ barrelIndexModuleInfoCache.set(filePath, {
25046
+ mtimeMs: fileStat.mtimeMs,
25047
+ size: fileStat.size,
25048
+ moduleInfo
25049
+ });
25050
+ return moduleInfo;
25051
+ };
25052
+ const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
25053
+ //#endregion
24289
25054
  //#region src/react-native-dependency-names.ts
24290
25055
  const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
24291
25056
  "expo",
@@ -24488,6 +25253,35 @@ const classifyReactNativeFileTarget = (context) => {
24488
25253
  };
24489
25254
  const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
24490
25255
  //#endregion
25256
+ //#region src/plugin/utils/resolve-barrel-export-file-path.ts
25257
+ const getUniqueFilePath = (filePaths) => {
25258
+ const uniqueFilePaths = new Set(filePaths);
25259
+ if (uniqueFilePaths.size !== 1) return null;
25260
+ const [filePath] = uniqueFilePaths;
25261
+ return filePath ?? null;
25262
+ };
25263
+ const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
25264
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
25265
+ if (!resolvedTargetPath) return null;
25266
+ const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
25267
+ if (nestedTargetPath) return nestedTargetPath;
25268
+ return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
25269
+ };
25270
+ const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
25271
+ if (visitedFilePaths.has(barrelFilePath)) return null;
25272
+ visitedFilePaths.add(barrelFilePath);
25273
+ const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
25274
+ if (!moduleInfo.isBarrel) return null;
25275
+ const target = moduleInfo.exportsByName.get(exportedName);
25276
+ if (target) {
25277
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
25278
+ if (!resolvedTargetPath) return null;
25279
+ return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
25280
+ }
25281
+ if (exportedName === "default") return null;
25282
+ return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
25283
+ };
25284
+ //#endregion
24491
25285
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
24492
25286
  const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
24493
25287
  const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
@@ -24817,7 +25611,7 @@ const isAsyncFunctionLike = (node) => {
24817
25611
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
24818
25612
  return false;
24819
25613
  };
24820
- const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
25614
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
24821
25615
  "forEach",
24822
25616
  "map",
24823
25617
  "filter",
@@ -24838,7 +25632,7 @@ const runsOnEffectDispatch = (functionNode) => {
24838
25632
  if (parent.callee === functionNode) return true;
24839
25633
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
24840
25634
  const callee = parent.callee;
24841
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
25635
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
24842
25636
  };
24843
25637
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
24844
25638
  const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
@@ -25782,57 +26576,6 @@ const noDefaultProps = defineRule({
25782
26576
  } })
25783
26577
  });
25784
26578
  //#endregion
25785
- //#region src/plugin/rules/state-and-effects/no-derived-state.ts
25786
- const getStateName$1 = (stateDeclarator) => {
25787
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
25788
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
25789
- const stateBinding = stateDeclarator.id.elements?.[0];
25790
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
25791
- const setterBinding = stateDeclarator.id.elements?.[1];
25792
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
25793
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
25794
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
25795
- };
25796
- const noDerivedState = defineRule({
25797
- id: "no-derived-state",
25798
- title: "Derived value copied into state",
25799
- severity: "warn",
25800
- tags: ["test-noise"],
25801
- recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into useState through a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
25802
- create: (context) => ({ CallExpression(node) {
25803
- if (!isUseEffect(node)) return;
25804
- const analysis = getProgramAnalysis(node);
25805
- if (!analysis) return;
25806
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
25807
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
25808
- const stateName = getStateName$1(fact.stateDeclarator);
25809
- context.report({
25810
- node: fact.callExpression,
25811
- message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
25812
- });
25813
- }
25814
- } })
25815
- });
25816
- //#endregion
25817
- //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
25818
- const noDerivedStateEffect = defineRule({
25819
- id: "no-derived-state-effect",
25820
- title: "Derived state stored in an effect",
25821
- severity: "warn",
25822
- tags: ["test-noise"],
25823
- 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",
25824
- create: (context) => ({ CallExpression(node) {
25825
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25826
- const analysis = getProgramAnalysis(node);
25827
- if (!analysis) return;
25828
- if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
25829
- context.report({
25830
- node,
25831
- message: "You pay an extra render for state you can derive from other values."
25832
- });
25833
- } })
25834
- });
25835
- //#endregion
25836
26579
  //#region src/plugin/utils/is-component-function.ts
25837
26580
  const isFunctionAssignedToComponent = (functionNode) => {
25838
26581
  let cursor = functionNode.parent ?? null;
@@ -25966,6 +26709,236 @@ const createComponentPropStackTracker = (callbacks) => {
25966
26709
  };
25967
26710
  };
25968
26711
  //#endregion
26712
+ //#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
26713
+ const findReferenceDeclarator = (reference) => {
26714
+ for (const definition of reference.resolved?.defs ?? []) {
26715
+ const definitionNode = definition.node;
26716
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
26717
+ }
26718
+ return null;
26719
+ };
26720
+ const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
26721
+ const node = stripParenExpression(expression);
26722
+ if (!isNodeOfType(node, "Identifier")) return null;
26723
+ const reference = getRef(analysis, node);
26724
+ if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
26725
+ if (isProp(analysis, reference)) {
26726
+ if (isWholePropsObjectReference(analysis, reference)) return null;
26727
+ return { bindingIdentity: reference.resolved };
26728
+ }
26729
+ if (isState(analysis, reference)) {
26730
+ const stateDeclarator = getUseStateDecl(analysis, reference);
26731
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
26732
+ return { bindingIdentity: reference.resolved };
26733
+ }
26734
+ if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
26735
+ const declarator = findReferenceDeclarator(reference);
26736
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
26737
+ const nextVisitedBindings = new Set(visitedBindings);
26738
+ nextVisitedBindings.add(reference.resolved);
26739
+ return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
26740
+ };
26741
+ const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
26742
+ const getDirectBranchStatements = (branch) => {
26743
+ if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
26744
+ return [branch];
26745
+ };
26746
+ const getDirectCallExpression = (statement) => {
26747
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26748
+ const expression = stripParenExpression(statement.expression);
26749
+ return isNodeOfType(expression, "CallExpression") ? expression : null;
26750
+ };
26751
+ const getDirectAssignmentExpression = (statement) => {
26752
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26753
+ const expression = stripParenExpression(statement.expression);
26754
+ return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
26755
+ };
26756
+ const findDirectStateSetterReference = (analysis, callExpression) => {
26757
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
26758
+ const callee = stripParenExpression(callExpression.callee);
26759
+ if (!isNodeOfType(callee, "Identifier")) return null;
26760
+ const reference = getRef(analysis, callee);
26761
+ return reference && isStateSetter(analysis, reference) ? reference : null;
26762
+ };
26763
+ const getStaticRefCurrentReference = (analysis, expression) => {
26764
+ const node = stripParenExpression(expression);
26765
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
26766
+ const reference = getRef(analysis, node.object);
26767
+ if (!reference) return null;
26768
+ const declarator = findReferenceDeclarator(reference);
26769
+ return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
26770
+ };
26771
+ const getStateTracker = (analysis, expression) => {
26772
+ const node = stripParenExpression(expression);
26773
+ if (!isNodeOfType(node, "Identifier")) return null;
26774
+ const reference = getRef(analysis, node);
26775
+ if (!reference || !isState(analysis, reference)) return null;
26776
+ const declarator = getUseStateDecl(analysis, reference);
26777
+ if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
26778
+ return {
26779
+ kind: "state",
26780
+ declarator
26781
+ };
26782
+ };
26783
+ const getRefTracker = (analysis, expression) => {
26784
+ const reference = getStaticRefCurrentReference(analysis, expression);
26785
+ return reference ? {
26786
+ kind: "ref",
26787
+ reference
26788
+ } : null;
26789
+ };
26790
+ const getTrackerInitializer = (tracker) => {
26791
+ const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
26792
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
26793
+ const initializer = declarator.init.arguments?.[0];
26794
+ return initializer ? initializer : null;
26795
+ };
26796
+ const isTrackerSynchronized = (analysis, guard) => {
26797
+ for (const statement of guard.statements) {
26798
+ if (guard.tracker.kind === "state") {
26799
+ const callExpression = getDirectCallExpression(statement);
26800
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26801
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26802
+ if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
26803
+ const writtenValue = callExpression.arguments?.[0];
26804
+ if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
26805
+ continue;
26806
+ }
26807
+ const assignmentExpression = getDirectAssignmentExpression(statement);
26808
+ if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
26809
+ if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
26810
+ if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
26811
+ }
26812
+ return false;
26813
+ };
26814
+ const parseRenderTrackerGuard = (analysis, statement) => {
26815
+ if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
26816
+ const left = statement.test.left;
26817
+ const right = statement.test.right;
26818
+ const candidates = [{
26819
+ sourceExpression: left,
26820
+ trackerExpression: right
26821
+ }, {
26822
+ sourceExpression: right,
26823
+ trackerExpression: left
26824
+ }];
26825
+ for (const candidate of candidates) {
26826
+ const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
26827
+ if (!source) continue;
26828
+ const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
26829
+ if (!tracker) continue;
26830
+ const trackerInitializer = getTrackerInitializer(tracker);
26831
+ if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
26832
+ const guard = {
26833
+ source,
26834
+ tracker,
26835
+ statements: getDirectBranchStatements(statement.consequent)
26836
+ };
26837
+ return isTrackerSynchronized(analysis, guard) ? guard : null;
26838
+ }
26839
+ return null;
26840
+ };
26841
+ const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
26842
+ if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
26843
+ const references = setterReference.resolved.references.filter((reference) => !reference.init);
26844
+ if (references.length !== 1) return false;
26845
+ return references[0].identifier === callExpression.callee;
26846
+ };
26847
+ const getStateInitializer = (stateDeclarator) => {
26848
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
26849
+ const initializer = stateDeclarator.init.arguments?.[0];
26850
+ return initializer ? initializer : null;
26851
+ };
26852
+ const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
26853
+ if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
26854
+ const componentFunction = componentBody.parent;
26855
+ const facts = [];
26856
+ for (const statement of componentBody.body ?? []) {
26857
+ const guard = parseRenderTrackerGuard(analysis, statement);
26858
+ if (!guard) continue;
26859
+ for (const branchStatement of guard.statements) {
26860
+ const callExpression = getDirectCallExpression(branchStatement);
26861
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26862
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26863
+ if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
26864
+ const stateDeclarator = getUseStateDecl(analysis, setterReference);
26865
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
26866
+ const writtenValue = callExpression.arguments?.[0];
26867
+ const initializer = getStateInitializer(stateDeclarator);
26868
+ if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
26869
+ facts.push({
26870
+ callExpression,
26871
+ stateDeclarator
26872
+ });
26873
+ }
26874
+ }
26875
+ return facts;
26876
+ };
26877
+ //#endregion
26878
+ //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26879
+ const getStateName$1 = (stateDeclarator) => {
26880
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26881
+ if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26882
+ const stateBinding = stateDeclarator.id.elements?.[0];
26883
+ if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26884
+ const setterBinding = stateDeclarator.id.elements?.[1];
26885
+ if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26886
+ if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26887
+ return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26888
+ };
26889
+ const noDerivedState = defineRule({
26890
+ id: "no-derived-state",
26891
+ title: "Derived value copied into state",
26892
+ severity: "warn",
26893
+ tags: ["test-noise"],
26894
+ recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26895
+ create: (context) => {
26896
+ const reportStateWrite = (callExpression, stateDeclarator) => {
26897
+ const stateName = getStateName$1(stateDeclarator);
26898
+ context.report({
26899
+ node: callExpression,
26900
+ message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26901
+ });
26902
+ };
26903
+ return {
26904
+ ...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
26905
+ if (!componentBody) return;
26906
+ const analysis = getProgramAnalysis(componentBody);
26907
+ if (!analysis) return;
26908
+ for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
26909
+ } }).visitors,
26910
+ CallExpression(node) {
26911
+ if (!isUseEffect(node)) return;
26912
+ const analysis = getProgramAnalysis(node);
26913
+ if (!analysis) return;
26914
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26915
+ if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26916
+ reportStateWrite(fact.callExpression, fact.stateDeclarator);
26917
+ }
26918
+ }
26919
+ };
26920
+ }
26921
+ });
26922
+ //#endregion
26923
+ //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26924
+ const noDerivedStateEffect = defineRule({
26925
+ id: "no-derived-state-effect",
26926
+ title: "Derived state stored in an effect",
26927
+ severity: "warn",
26928
+ tags: ["test-noise"],
26929
+ 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",
26930
+ create: (context) => ({ CallExpression(node) {
26931
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26932
+ const analysis = getProgramAnalysis(node);
26933
+ if (!analysis) return;
26934
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26935
+ context.report({
26936
+ node,
26937
+ message: "You pay an extra render for state you can derive from other values."
26938
+ });
26939
+ } })
26940
+ });
26941
+ //#endregion
25969
26942
  //#region src/plugin/utils/is-initial-only-prop-name.ts
25970
26943
  const isInitialOnlyPropName = (propName) => {
25971
26944
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -27471,14 +28444,14 @@ const hasDocumentClassListMutation = (node) => {
27471
28444
  //#endregion
27472
28445
  //#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
27473
28446
  const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
27474
- const unwrapChainExpression$1 = (node) => {
28447
+ const unwrapChainExpression$2 = (node) => {
27475
28448
  if (!node) return null;
27476
28449
  if (isNodeOfType(node, "ChainExpression")) return node.expression;
27477
28450
  return node;
27478
28451
  };
27479
28452
  const collectGuardExpressions = (node, into) => {
27480
28453
  if (!node) return;
27481
- const unwrappedNode = unwrapChainExpression$1(node);
28454
+ const unwrappedNode = unwrapChainExpression$2(node);
27482
28455
  if (!unwrappedNode) return;
27483
28456
  const rootIdentifierName = getRootIdentifierName(unwrappedNode);
27484
28457
  if (rootIdentifierName) {
@@ -27509,7 +28482,7 @@ const isReturnOnlyStatement = (node) => {
27509
28482
  };
27510
28483
  const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
27511
28484
  const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
27512
- const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
28485
+ const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
27513
28486
  if (!unwrappedDependencyExpression) return false;
27514
28487
  if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
27515
28488
  return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
@@ -29329,28 +30302,26 @@ const noGiantComponent = defineRule({
29329
30302
  const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
29330
30303
  return lineCount > 300 ? lineCount : null;
29331
30304
  };
29332
- const reportOversizedComponent = (nameNode, componentName, lineCount) => {
30305
+ const reportOversizedComponent = (nameNode, componentName) => {
29333
30306
  context.report({
29334
30307
  node: nameNode,
29335
- message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
30308
+ message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
29336
30309
  });
29337
30310
  };
29338
30311
  return {
29339
30312
  FunctionDeclaration(node) {
29340
30313
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
29341
- const lineCount = getOversizedComponentLineCount(node);
29342
- if (lineCount === null) return;
30314
+ if (getOversizedComponentLineCount(node) === null) return;
29343
30315
  if (!functionContainsReactRenderOutput(node, context.scopes)) return;
29344
- reportOversizedComponent(node.id, node.id.name, lineCount);
30316
+ reportOversizedComponent(node.id, node.id.name);
29345
30317
  },
29346
30318
  VariableDeclarator(node) {
29347
30319
  if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
29348
30320
  const functionNode = unwrapReactHocFunction(node.init);
29349
30321
  if (!functionNode) return;
29350
- const lineCount = getOversizedComponentLineCount(functionNode);
29351
- if (lineCount === null) return;
30322
+ if (getOversizedComponentLineCount(functionNode) === null) return;
29352
30323
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
29353
- reportOversizedComponent(node.id, node.id.name, lineCount);
30324
+ reportOversizedComponent(node.id, node.id.name);
29354
30325
  }
29355
30326
  };
29356
30327
  }
@@ -29568,57 +30539,6 @@ const noGrayOnColoredBackground = defineRule({
29568
30539
  } })
29569
30540
  });
29570
30541
  //#endregion
29571
- //#region src/plugin/utils/find-declarator-for-binding.ts
29572
- const findDeclaratorForBinding = (bindingIdentifier) => {
29573
- let ancestor = bindingIdentifier.parent;
29574
- while (ancestor) {
29575
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29576
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29577
- ancestor = ancestor.parent ?? null;
29578
- }
29579
- return null;
29580
- };
29581
- //#endregion
29582
- //#region src/plugin/utils/executes-during-render.ts
29583
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29584
- "map",
29585
- "filter",
29586
- "forEach",
29587
- "flatMap",
29588
- "reduce",
29589
- "reduceRight",
29590
- "some",
29591
- "every",
29592
- "find",
29593
- "findIndex",
29594
- "findLast",
29595
- "findLastIndex",
29596
- "sort",
29597
- "toSorted"
29598
- ]);
29599
- const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
29600
- const isGlobalArrayFromMember = (node, scopes) => {
29601
- const unwrappedNode = stripParenExpression(node);
29602
- if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
29603
- return unwrappedNode.property.name === "from";
29604
- };
29605
- const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
29606
- const unwrappedNode = stripParenExpression(node);
29607
- if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
29608
- const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
29609
- if (!binding?.initializer) return false;
29610
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29611
- return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
29612
- };
29613
- const executesDuringRender = (functionNode, scopes) => {
29614
- const parent = functionNode.parent;
29615
- if (!isNodeOfType(parent, "CallExpression")) return false;
29616
- if (parent.callee === functionNode) return true;
29617
- if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
29618
- if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
29619
- return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
29620
- };
29621
- //#endregion
29622
30542
  //#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
29623
30543
  const findEnclosingJsxOpeningElement = (node) => {
29624
30544
  let cursor = node.parent;
@@ -29630,17 +30550,6 @@ const findEnclosingJsxOpeningElement = (node) => {
29630
30550
  return null;
29631
30551
  };
29632
30552
  //#endregion
29633
- //#region src/plugin/utils/find-render-phase-component-or-hook.ts
29634
- const findRenderPhaseComponentOrHook = (node, scopes) => {
29635
- let functionNode = findEnclosingFunction(node);
29636
- while (functionNode) {
29637
- if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29638
- if (!executesDuringRender(functionNode, scopes)) return null;
29639
- functionNode = findEnclosingFunction(functionNode);
29640
- }
29641
- return null;
29642
- };
29643
- //#endregion
29644
30553
  //#region src/plugin/utils/has-client-render-evidence.ts
29645
30554
  const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29646
30555
  if (fileHasUseClientDirective) return true;
@@ -29749,9 +30658,6 @@ const isGatedByFalsyInitialState = (node, scopes) => {
29749
30658
  return false;
29750
30659
  };
29751
30660
  //#endregion
29752
- //#region src/plugin/utils/is-event-handler-attribute.ts
29753
- const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
29754
- //#endregion
29755
30661
  //#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
29756
30662
  const evaluateEquality = (operator, left, right) => {
29757
30663
  if (operator === "===" || operator === "==") return left === right;
@@ -30052,6 +30958,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
30052
30958
  } })
30053
30959
  });
30054
30960
  //#endregion
30961
+ //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
30962
+ const TIMER_FUNCTION_NAMES = new Set([
30963
+ "cancelAnimationFrame",
30964
+ "clearInterval",
30965
+ "clearTimeout",
30966
+ "queueMicrotask",
30967
+ "requestAnimationFrame",
30968
+ "setInterval",
30969
+ "setTimeout"
30970
+ ]);
30971
+ const STORAGE_MUTATION_METHOD_NAMES = new Set([
30972
+ "clear",
30973
+ "removeItem",
30974
+ "setItem"
30975
+ ]);
30976
+ const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
30977
+ const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
30978
+ const NOTIFICATION_RECEIVER_NAMES = new Set([
30979
+ "message",
30980
+ "notification",
30981
+ "toast"
30982
+ ]);
30983
+ const NOTIFICATION_METHOD_NAMES = new Set([
30984
+ "error",
30985
+ "info",
30986
+ "loading",
30987
+ "open",
30988
+ "show",
30989
+ "success",
30990
+ "warning"
30991
+ ]);
30992
+ const getMemberCall = (node) => {
30993
+ if (!isNodeOfType(node, "CallExpression")) return null;
30994
+ if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
30995
+ if (!isNodeOfType(node.callee.property, "Identifier")) return null;
30996
+ return {
30997
+ methodName: node.callee.property.name,
30998
+ receiver: stripParenExpression(node.callee.object)
30999
+ };
31000
+ };
31001
+ const isNotificationReceiver = (receiver, scopes) => {
31002
+ if (!isNodeOfType(receiver, "Identifier")) return false;
31003
+ if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
31004
+ const symbol = scopes.symbolFor(receiver);
31005
+ if (symbol?.kind === "import") return true;
31006
+ if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
31007
+ const callee = symbol.initializer.callee;
31008
+ return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
31009
+ };
31010
+ const getKnownImpureCall = (callExpression, scopes) => {
31011
+ if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
31012
+ const memberCall = getMemberCall(callExpression);
31013
+ if (!memberCall) return null;
31014
+ const { methodName, receiver } = memberCall;
31015
+ if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
31016
+ if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
31017
+ if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
31018
+ return null;
31019
+ };
31020
+ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
31021
+ let rootIdentifier = null;
31022
+ if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
31023
+ else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
31024
+ if (!rootIdentifier) return null;
31025
+ const updaterScope = scopes.ownScopeFor(updater);
31026
+ if (!updaterScope) return null;
31027
+ const symbol = scopes.symbolFor(rootIdentifier);
31028
+ if (!symbol) return `the external value "${rootIdentifier.name}"`;
31029
+ if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
31030
+ return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
31031
+ };
31032
+ const findImpureUpdaterOperation = (updater, scopes) => {
31033
+ const analysis = getProgramAnalysis(updater);
31034
+ let operation = null;
31035
+ walkAst(updater, (child) => {
31036
+ if (operation) return false;
31037
+ if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
31038
+ if (isNodeOfType(child, "CallExpression")) {
31039
+ if (isNodeOfType(child.callee, "Identifier") && analysis) {
31040
+ const calleeReference = getRef(analysis, child.callee);
31041
+ if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
31042
+ operation = `the nested state update "${child.callee.name}()"`;
31043
+ return false;
31044
+ }
31045
+ }
31046
+ const impureCall = getKnownImpureCall(child, scopes);
31047
+ if (impureCall) {
31048
+ operation = impureCall;
31049
+ return false;
31050
+ }
31051
+ }
31052
+ if (isNodeOfType(child, "AssignmentExpression")) {
31053
+ operation = getExternalAssignmentDescription(child.left, updater, scopes);
31054
+ if (operation) return false;
31055
+ }
31056
+ if (isNodeOfType(child, "UpdateExpression")) {
31057
+ operation = getExternalAssignmentDescription(child.argument, updater, scopes);
31058
+ if (operation) return false;
31059
+ }
31060
+ });
31061
+ return operation;
31062
+ };
31063
+ const noImpureStateUpdater = defineRule({
31064
+ id: "no-impure-state-updater",
31065
+ title: "State updater has side effects",
31066
+ severity: "error",
31067
+ recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
31068
+ create: (context) => ({ CallExpression(node) {
31069
+ const updater = node.arguments?.[0];
31070
+ if (!updater || !isFunctionLike$1(updater)) return;
31071
+ const analysis = getProgramAnalysis(node);
31072
+ if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
31073
+ const calleeReference = getRef(analysis, node.callee);
31074
+ if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
31075
+ const stateDeclarator = getUseStateDecl(analysis, calleeReference);
31076
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
31077
+ const operation = findImpureUpdaterOperation(updater, context.scopes);
31078
+ if (!operation) return;
31079
+ context.report({
31080
+ node: updater,
31081
+ message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
31082
+ });
31083
+ } })
31084
+ });
31085
+ //#endregion
30055
31086
  //#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
30056
31087
  const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
30057
31088
  const REACT_USE_REF_OPTIONS = {
@@ -30218,7 +31249,7 @@ const noInitializeState = defineRule({
30218
31249
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
30219
31250
  const analysis = getProgramAnalysis(node);
30220
31251
  if (!analysis) return;
30221
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
31252
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
30222
31253
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
30223
31254
  const stateName = getStateName(fact.stateDeclarator);
30224
31255
  context.report({
@@ -31850,6 +32881,41 @@ const noMutableInDeps = defineRule({
31850
32881
  }
31851
32882
  });
31852
32883
  //#endregion
32884
+ //#region src/plugin/utils/is-result-discarded-call.ts
32885
+ const isResultDiscardedCall = (callExpression) => {
32886
+ let node = callExpression;
32887
+ let parent = node.parent;
32888
+ while (parent) {
32889
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
32890
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
32891
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
32892
+ if (isNodeOfType(parent, "ChainExpression")) {
32893
+ node = parent;
32894
+ parent = node.parent;
32895
+ continue;
32896
+ }
32897
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
32898
+ node = parent;
32899
+ parent = node.parent;
32900
+ continue;
32901
+ }
32902
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
32903
+ node = parent;
32904
+ parent = node.parent;
32905
+ continue;
32906
+ }
32907
+ if (isNodeOfType(parent, "SequenceExpression")) {
32908
+ const expressions = parent.expressions ?? [];
32909
+ if (expressions[expressions.length - 1] !== node) return true;
32910
+ node = parent;
32911
+ parent = node.parent;
32912
+ continue;
32913
+ }
32914
+ return false;
32915
+ }
32916
+ return false;
32917
+ };
32918
+ //#endregion
31853
32919
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
31854
32920
  const LODASH_MUTATOR_NAMES = new Set([
31855
32921
  "set",
@@ -33140,7 +34206,7 @@ const isUseRefIdentifier = (identifier) => {
33140
34206
  };
33141
34207
  const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
33142
34208
  const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
33143
- const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
34209
+ const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
33144
34210
  const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
33145
34211
  "useCallback",
33146
34212
  "useMemo",
@@ -33175,7 +34241,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
33175
34241
  return Boolean(ref.resolved?.defs.some((def) => {
33176
34242
  const node = def.node;
33177
34243
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
33178
- const initializer = unwrapChainExpression(node.init);
34244
+ const initializer = unwrapChainExpression$1(node.init);
33179
34245
  const wrappedFunction = getWrapperHookWrappedFunction(initializer);
33180
34246
  if (wrappedFunction) {
33181
34247
  if (wrappedFunction.async) return false;
@@ -33185,10 +34251,170 @@ const isDirectParentCallbackRef = (analysis, ref) => {
33185
34251
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
33186
34252
  }));
33187
34253
  };
34254
+ const getDeclarationKind = (declarator) => {
34255
+ const declaration = declarator.parent;
34256
+ return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
34257
+ };
34258
+ const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
34259
+ const getDestructuredPropertyName = (bindingIdentifier) => {
34260
+ const property = bindingIdentifier.parent;
34261
+ if (!property || !isNodeOfType(property, "Property")) return null;
34262
+ if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
34263
+ return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
34264
+ };
34265
+ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
34266
+ const unwrappedExpression = stripParenExpression(expression);
34267
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
34268
+ const callbackReference = getRef(analysis, unwrappedExpression);
34269
+ const callbackVariable = callbackReference?.resolved;
34270
+ if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
34271
+ if (isProp(analysis, callbackReference)) {
34272
+ if (isWholePropsObjectReference(analysis, callbackReference)) return null;
34273
+ const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
34274
+ return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
34275
+ }
34276
+ if (hasMutableBindingWrite(callbackReference)) return null;
34277
+ const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34278
+ if (definitions.length !== 1) return null;
34279
+ const declarator = definitions[0];
34280
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
34281
+ visitedVariables.add(callbackVariable);
34282
+ if (isNodeOfType(declarator.id, "ObjectPattern")) {
34283
+ const bindingIdentifier = callbackVariable.defs[0]?.name;
34284
+ const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
34285
+ const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
34286
+ return propertyName && propsReference ? propertyName : null;
34287
+ }
34288
+ if (!isNodeOfType(declarator.id, "Identifier")) return null;
34289
+ return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
34290
+ }
34291
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
34292
+ const callbackName = getStaticMemberPropertyName(unwrappedExpression);
34293
+ if (!callbackName) return null;
34294
+ const receiver = stripParenExpression(unwrappedExpression.object);
34295
+ if (!isNodeOfType(receiver, "Identifier")) return null;
34296
+ const receiverReference = getRef(analysis, receiver);
34297
+ if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
34298
+ return callbackName;
34299
+ };
34300
+ const isNullishRefInitializer = (initializer) => {
34301
+ if (!initializer) return true;
34302
+ const unwrappedInitializer = stripParenExpression(initializer);
34303
+ if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
34304
+ if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
34305
+ return unwrappedInitializer.name === "undefined";
34306
+ };
34307
+ const getDirectComponentBodyStatement = (node, componentBody) => {
34308
+ let current = node;
34309
+ while (current?.parent && current.parent !== componentBody) current = current.parent;
34310
+ return current?.parent === componentBody ? current : null;
34311
+ };
34312
+ const getVariableForDeclarator = (analysis, declarator) => {
34313
+ for (const scope of analysis.scopeManager.scopes) {
34314
+ const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
34315
+ if (variable) return variable;
34316
+ }
34317
+ return null;
34318
+ };
34319
+ const getRefMember = (identifier) => {
34320
+ const receiver = findTransparentExpressionRoot(identifier);
34321
+ const member = receiver.parent;
34322
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
34323
+ return member;
34324
+ };
34325
+ const getRefMemberAssignment = (identifier) => {
34326
+ const member = getRefMember(identifier);
34327
+ if (!member) return null;
34328
+ const memberRoot = findTransparentExpressionRoot(member);
34329
+ const assignment = memberRoot.parent;
34330
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
34331
+ return assignment;
34332
+ };
34333
+ const getRefAliasDeclarator = (identifier) => {
34334
+ const initializer = findTransparentExpressionRoot(identifier);
34335
+ const declarator = initializer.parent;
34336
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
34337
+ return declarator;
34338
+ };
34339
+ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
34340
+ if (!isNodeOfType(receiver, "Identifier")) return null;
34341
+ const receiverReference = getRef(analysis, receiver);
34342
+ if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
34343
+ const variables = /* @__PURE__ */ new Set();
34344
+ let currentVariable = receiverReference.resolved;
34345
+ let refCall = null;
34346
+ while (currentVariable && !variables.has(currentVariable)) {
34347
+ variables.add(currentVariable);
34348
+ const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34349
+ if (definitions.length !== 1) return null;
34350
+ const declarator = definitions[0];
34351
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
34352
+ if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
34353
+ refCall = declarator.init;
34354
+ break;
34355
+ }
34356
+ if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
34357
+ const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
34358
+ if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
34359
+ currentVariable = upstreamReference.resolved;
34360
+ }
34361
+ if (!refCall) return null;
34362
+ const pendingVariables = [...variables];
34363
+ while (pendingVariables.length > 0) {
34364
+ const variable = pendingVariables.pop();
34365
+ if (!variable) continue;
34366
+ for (const candidateReference of variable.references) {
34367
+ const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
34368
+ if (!aliasDeclarator) continue;
34369
+ const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
34370
+ if (!aliasVariable || variables.has(aliasVariable)) continue;
34371
+ if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
34372
+ variables.add(aliasVariable);
34373
+ pendingVariables.push(aliasVariable);
34374
+ }
34375
+ }
34376
+ return {
34377
+ refCall,
34378
+ variables
34379
+ };
34380
+ };
34381
+ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
34382
+ const callee = stripParenExpression(callExpression.callee);
34383
+ if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
34384
+ const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
34385
+ if (!bindingProvenance) return null;
34386
+ const { refCall, variables } = bindingProvenance;
34387
+ const componentFunction = findEnclosingFunction(effectCall);
34388
+ if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
34389
+ if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
34390
+ const callbackPropNames = /* @__PURE__ */ new Set();
34391
+ const initializer = refCall.arguments?.[0];
34392
+ const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
34393
+ if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
34394
+ else if (!isNullishRefInitializer(initializer)) return null;
34395
+ for (const variable of variables) for (const candidateReference of variable.references) {
34396
+ if (candidateReference.init) continue;
34397
+ if (candidateReference.isWrite()) return null;
34398
+ const identifier = candidateReference.identifier;
34399
+ if (getRefAliasDeclarator(identifier)) continue;
34400
+ const member = getRefMember(identifier);
34401
+ if (!member || getStaticMemberPropertyName(member) !== "current") return null;
34402
+ const memberParent = findTransparentExpressionRoot(member).parent;
34403
+ if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
34404
+ const assignment = getRefMemberAssignment(identifier);
34405
+ if (!assignment) continue;
34406
+ const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
34407
+ if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
34408
+ const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
34409
+ if (!assignedCallbackName) return null;
34410
+ callbackPropNames.add(assignedCallbackName);
34411
+ }
34412
+ return callbackPropNames.size > 0 ? { callbackPropNames } : null;
34413
+ };
33188
34414
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
33189
34415
  const node = def.node;
33190
34416
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
33191
- return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
34417
+ return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
33192
34418
  }));
33193
34419
  const isHandlerBagArgument = (analysis, argument) => {
33194
34420
  if (!isNodeOfType(argument, "ObjectExpression")) return false;
@@ -33210,7 +34436,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
33210
34436
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
33211
34437
  const node = def.node;
33212
34438
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
33213
- const init = unwrapChainExpression(node.init);
34439
+ const init = unwrapChainExpression$1(node.init);
33214
34440
  if (!isNodeOfType(init, "CallExpression")) return false;
33215
34441
  const callee = init.callee;
33216
34442
  if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
@@ -33240,71 +34466,79 @@ const noPassDataToParent = defineRule({
33240
34466
  severity: "warn",
33241
34467
  tags: ["test-noise"],
33242
34468
  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",
33243
- create: (context) => ({ CallExpression(node) {
33244
- if (!isUseEffect(node)) return;
33245
- const analysis = getProgramAnalysis(node);
33246
- if (!analysis) return;
33247
- if (hasCleanup(analysis, node)) return;
33248
- const effectFnRefs = getEffectFnRefs(analysis, node);
33249
- if (!effectFnRefs) return;
33250
- const effectFn = getEffectFn(analysis, node);
33251
- if (!effectFn) return;
33252
- for (const ref of effectFnRefs) {
33253
- const callExpr = getCallExpr(ref);
33254
- if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
33255
- if (isRefCall(analysis, ref)) continue;
33256
- if (!isSynchronous(ref.identifier, effectFn)) continue;
33257
- const calleeNode = unwrapChainExpression(callExpr.callee);
33258
- const identifier = ref.identifier;
33259
- if (calleeNode === identifier) {
33260
- if (!isDirectParentCallbackRef(analysis, ref)) continue;
33261
- if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
33262
- } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
33263
- if (!isWholePropsObjectReference(analysis, ref)) continue;
33264
- if (isCustomHookParameter(ref)) continue;
33265
- } else continue;
33266
- const methodName = getCallMethodName(calleeNode);
33267
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
33268
- if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
33269
- if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
33270
- if (isNamespacedApiCallee(calleeNode)) continue;
33271
- const calleeName = isNodeOfType(identifier, "Identifier") ? identifier.name : methodName;
33272
- const isSetterNamedCallee = Boolean(calleeName && SETTER_NAMED_PROP_PATTERN.test(calleeName));
33273
- const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
33274
- const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
33275
- if (isFunctionLike$1(argument)) {
33276
- if (!isSetterNamedCallee) return [];
33277
- return getFunctionalUpdaterDataRefs(analysis, argument);
33278
- }
33279
- if (isHandlerBagArgument(analysis, argument)) return [];
33280
- if (isParentWiredHookResultArgument(analysis, argument)) return [];
33281
- if (isNodeOfType(argument, "Identifier")) {
33282
- const argumentRef = getRef(analysis, argument);
33283
- if (argumentRef && resolveToFunction(argumentRef)) return [];
33284
- }
33285
- return getDownstreamRefs(analysis, argument);
33286
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
33287
- if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
33288
- if (!argsUpstreamRefs.some((argRef) => {
33289
- if (isUseStateIdentifier(argRef.identifier)) return false;
33290
- if (isProp(analysis, argRef)) return false;
33291
- if (isUseRefIdentifier(argRef.identifier)) return false;
33292
- if (isRefCurrent(argRef)) return false;
33293
- if (isConstant(argRef)) return false;
33294
- if (isParentWiredHookResultRef(analysis, argRef)) return false;
33295
- if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
33296
- if (resolvesToFunctionBinding(argRef)) return false;
33297
- const argIdentifier = argRef.identifier;
33298
- if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
33299
- if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
33300
- return true;
33301
- })) continue;
33302
- context.report({
33303
- node: callExpr,
33304
- message: "Handing data back to a parent from a useEffect costs your users an extra render."
33305
- });
33306
- }
33307
- } })
34469
+ create: (context) => {
34470
+ const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
34471
+ allowGlobalReactNamespace: true,
34472
+ allowUnboundBareCalls: true
34473
+ });
34474
+ return { CallExpression(node) {
34475
+ if (!isUseEffect(node)) return;
34476
+ const analysis = getProgramAnalysis(node);
34477
+ if (!analysis) return;
34478
+ if (hasCleanup(analysis, node)) return;
34479
+ const effectFnRefs = getEffectFnRefs(analysis, node);
34480
+ if (!effectFnRefs) return;
34481
+ const effectFn = getEffectFn(analysis, node);
34482
+ if (!effectFn) return;
34483
+ for (const ref of effectFnRefs) {
34484
+ const callExpr = getCallExpr(ref);
34485
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
34486
+ const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
34487
+ if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
34488
+ if (!isSynchronous(ref.identifier, effectFn)) continue;
34489
+ const calleeNode = unwrapChainExpression$1(callExpr.callee);
34490
+ const identifier = ref.identifier;
34491
+ if (callbackRefProvenance) {
34492
+ if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
34493
+ } else if (calleeNode === identifier) {
34494
+ if (!isDirectParentCallbackRef(analysis, ref)) continue;
34495
+ if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
34496
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
34497
+ if (!isWholePropsObjectReference(analysis, ref)) continue;
34498
+ if (isCustomHookParameter(ref)) continue;
34499
+ } else continue;
34500
+ const methodName = getCallMethodName(calleeNode);
34501
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
34502
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
34503
+ if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
34504
+ if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
34505
+ const isSetterNamedCallee = callbackRefProvenance ? [...callbackRefProvenance.callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
34506
+ const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
34507
+ const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
34508
+ if (isFunctionLike$1(argument)) {
34509
+ if (!isSetterNamedCallee) return [];
34510
+ return getFunctionalUpdaterDataRefs(analysis, argument);
34511
+ }
34512
+ if (isHandlerBagArgument(analysis, argument)) return [];
34513
+ if (isParentWiredHookResultArgument(analysis, argument)) return [];
34514
+ if (isNodeOfType(argument, "Identifier")) {
34515
+ const argumentRef = getRef(analysis, argument);
34516
+ if (argumentRef && resolveToFunction(argumentRef)) return [];
34517
+ }
34518
+ return getDownstreamRefs(analysis, argument);
34519
+ }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34520
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34521
+ if (!argsUpstreamRefs.some((argRef) => {
34522
+ if (isUseStateIdentifier(argRef.identifier)) return false;
34523
+ if (isProp(analysis, argRef)) return false;
34524
+ if (isUseRefIdentifier(argRef.identifier)) return false;
34525
+ if (isRefCurrent(argRef)) return false;
34526
+ if (isConstant(argRef)) return false;
34527
+ if (isParentWiredHookResultRef(analysis, argRef)) return false;
34528
+ if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
34529
+ if (resolvesToFunctionBinding(argRef)) return false;
34530
+ const argIdentifier = argRef.identifier;
34531
+ if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
34532
+ if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
34533
+ return true;
34534
+ })) continue;
34535
+ context.report({
34536
+ node: callExpr,
34537
+ message: "Handing data back to a parent from a useEffect costs your users an extra render."
34538
+ });
34539
+ }
34540
+ } };
34541
+ }
33308
34542
  });
33309
34543
  //#endregion
33310
34544
  //#region src/plugin/utils/is-call-result-consumed-as-argument.ts
@@ -33858,6 +35092,66 @@ const noPropCallbackInEffect = defineRule({
33858
35092
  }
33859
35093
  });
33860
35094
  //#endregion
35095
+ //#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
35096
+ const isPreservedThroughConciseArrow = (callExpression, scopes) => {
35097
+ let node = callExpression;
35098
+ let parent = node.parent;
35099
+ while (parent) {
35100
+ if (isNodeOfType(parent, "ChainExpression")) {
35101
+ node = parent;
35102
+ parent = node.parent;
35103
+ continue;
35104
+ }
35105
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
35106
+ node = parent;
35107
+ parent = node.parent;
35108
+ continue;
35109
+ }
35110
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
35111
+ node = parent;
35112
+ parent = node.parent;
35113
+ continue;
35114
+ }
35115
+ if (isNodeOfType(parent, "SequenceExpression")) {
35116
+ const expressions = parent.expressions ?? [];
35117
+ if (expressions[expressions.length - 1] !== node) return false;
35118
+ node = parent;
35119
+ parent = node.parent;
35120
+ continue;
35121
+ }
35122
+ if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
35123
+ const invocation = parent.parent;
35124
+ if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
35125
+ if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
35126
+ const callee = stripParenExpression(invocation.callee);
35127
+ return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
35128
+ }
35129
+ node = invocation;
35130
+ parent = node.parent;
35131
+ }
35132
+ return false;
35133
+ };
35134
+ const noPropCallbackInRender = defineRule({
35135
+ id: "no-prop-callback-in-render",
35136
+ title: "Prop callback invoked during render",
35137
+ severity: "error",
35138
+ recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
35139
+ create: (context) => ({ CallExpression(node) {
35140
+ if (!isResultDiscardedCall(node)) return;
35141
+ if (isPreservedThroughConciseArrow(node, context.scopes)) return;
35142
+ if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
35143
+ const analysis = getProgramAnalysis(node);
35144
+ if (!analysis) return;
35145
+ const callee = stripParenExpression(node.callee);
35146
+ if (isFunctionLike$1(callee)) return;
35147
+ if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
35148
+ context.report({
35149
+ node,
35150
+ message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
35151
+ });
35152
+ } })
35153
+ });
35154
+ //#endregion
33861
35155
  //#region src/plugin/rules/architecture/no-prop-types.ts
33862
35156
  const PROP_TYPES_PROPERTY = "propTypes";
33863
35157
  const isPropTypesKey = (key, computed) => {
@@ -34552,6 +35846,63 @@ const noRedundantShouldComponentUpdate = defineRule({
34552
35846
  }
34553
35847
  });
34554
35848
  //#endregion
35849
+ //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
35850
+ const resolveReactRefSymbol = (memberExpression, scopes) => {
35851
+ const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
35852
+ if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
35853
+ const symbol = resolveConstIdentifierAlias(receiver, scopes);
35854
+ if (!symbol?.initializer) return null;
35855
+ const initializer = stripParenExpression(symbol.initializer);
35856
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
35857
+ return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
35858
+ };
35859
+ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
35860
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
35861
+ const receiver = stripParenExpression(node.object);
35862
+ return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
35863
+ };
35864
+ const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
35865
+ if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
35866
+ let descendant = assignmentExpression;
35867
+ let ancestor = descendant.parent;
35868
+ while (ancestor) {
35869
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
35870
+ const { left, right } = ancestor.test;
35871
+ if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
35872
+ }
35873
+ descendant = ancestor;
35874
+ ancestor = descendant.parent;
35875
+ }
35876
+ return false;
35877
+ };
35878
+ const noRefCurrentInRender = defineRule({
35879
+ id: "no-ref-current-in-render",
35880
+ title: "Ref mutated during render",
35881
+ severity: "error",
35882
+ recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
35883
+ create: (context) => {
35884
+ const report = (memberExpression) => {
35885
+ if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
35886
+ if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
35887
+ context.report({
35888
+ node: memberExpression,
35889
+ message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
35890
+ });
35891
+ };
35892
+ return {
35893
+ AssignmentExpression(node) {
35894
+ const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
35895
+ if (!refSymbol) return;
35896
+ if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
35897
+ report(node.left);
35898
+ },
35899
+ UpdateExpression(node) {
35900
+ report(node.argument);
35901
+ }
35902
+ };
35903
+ }
35904
+ });
35905
+ //#endregion
34555
35906
  //#region src/plugin/rules/architecture/no-render-in-render.ts
34556
35907
  const isInsideComponentContext = (node) => {
34557
35908
  let cursor = node.parent;
@@ -40542,6 +41893,123 @@ const preferUseEffectEvent = defineRule({
40542
41893
  }
40543
41894
  });
40544
41895
  //#endregion
41896
+ //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
41897
+ const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
41898
+ "each",
41899
+ "every",
41900
+ "filter",
41901
+ "find",
41902
+ "findIndex",
41903
+ "findLast",
41904
+ "findLastIndex",
41905
+ "flatMap",
41906
+ "forEach",
41907
+ "map",
41908
+ "reduce",
41909
+ "reduceRight",
41910
+ "some",
41911
+ "sort",
41912
+ "toSorted"
41913
+ ]);
41914
+ const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
41915
+ "from",
41916
+ "fromAsync",
41917
+ "groupBy"
41918
+ ]);
41919
+ const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
41920
+ const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
41921
+ const isListenerRemovalViaNullHandler = (callNode) => {
41922
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
41923
+ const callee = unwrapChainExpression(callNode.callee);
41924
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
41925
+ };
41926
+ const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
41927
+ "off",
41928
+ "removeEventListener",
41929
+ "removeListener"
41930
+ ]);
41931
+ const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
41932
+ if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
41933
+ const handlerArgument = callNode.arguments?.[1];
41934
+ return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
41935
+ };
41936
+ const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41937
+ const callNode = unwrapChainExpression(node);
41938
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
41939
+ if (isListenerRemovalViaNullHandler(callNode)) return true;
41940
+ const callee = unwrapChainExpression(callNode.callee);
41941
+ if (isNodeOfType(callee, "Identifier")) {
41942
+ if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
41943
+ if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
41944
+ if (knownCleanupFunctionNames.has(callee.name)) return true;
41945
+ return false;
41946
+ }
41947
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
41948
+ if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
41949
+ if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
41950
+ return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
41951
+ }
41952
+ return false;
41953
+ };
41954
+ const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
41955
+ const isIteratorCallbackArgument = (node) => {
41956
+ const parentNode = node.parent;
41957
+ if (!isNodeOfType(parentNode, "CallExpression")) return false;
41958
+ if (!parentNode.arguments?.some((argument) => argument === node)) return false;
41959
+ const callee = unwrapChainExpression(parentNode.callee);
41960
+ if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
41961
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
41962
+ };
41963
+ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41964
+ let didFindRelease = false;
41965
+ walkAst(node, (child) => {
41966
+ if (didFindRelease) return false;
41967
+ if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
41968
+ if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
41969
+ didFindRelease = true;
41970
+ return false;
41971
+ }
41972
+ });
41973
+ return didFindRelease;
41974
+ };
41975
+ const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41976
+ if (!isFunctionLike$1(node)) return false;
41977
+ return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
41978
+ };
41979
+ const isProvablyNoOpCleanupFunction = (node) => {
41980
+ if (!isFunctionLike$1(node)) return false;
41981
+ let sawNoOpRemoval = false;
41982
+ let sawOtherCall = false;
41983
+ walkAst(node.body, (child) => {
41984
+ if (sawOtherCall) return false;
41985
+ if (isFunctionLike$1(child)) return false;
41986
+ const callNode = unwrapChainExpression(child);
41987
+ if (!isNodeOfType(callNode, "CallExpression")) return;
41988
+ const callee = unwrapChainExpression(callNode.callee);
41989
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
41990
+ sawNoOpRemoval = true;
41991
+ return;
41992
+ }
41993
+ sawOtherCall = true;
41994
+ });
41995
+ return sawNoOpRemoval && !sawOtherCall;
41996
+ };
41997
+ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
41998
+ if (!returnedValue) return false;
41999
+ const unwrappedValue = unwrapChainExpression(returnedValue);
42000
+ if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
42001
+ if (isNodeOfType(unwrappedValue, "Identifier")) {
42002
+ if (unwrappedValue.name === "undefined") return false;
42003
+ if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
42004
+ return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
42005
+ }
42006
+ if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
42007
+ if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
42008
+ if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
42009
+ if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
42010
+ return false;
42011
+ };
42012
+ //#endregion
40545
42013
  //#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
40546
42014
  const findUseEffectsInComponent = (componentBody) => {
40547
42015
  const effectCalls = [];
@@ -55397,6 +56865,18 @@ const reactDoctorRules = [
55397
56865
  requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
55398
56866
  }
55399
56867
  },
56868
+ {
56869
+ key: "react-doctor/no-impure-state-updater",
56870
+ id: "no-impure-state-updater",
56871
+ source: "react-doctor",
56872
+ originallyExternal: false,
56873
+ rule: {
56874
+ ...noImpureStateUpdater,
56875
+ framework: "global",
56876
+ category: "Bugs",
56877
+ requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
56878
+ }
56879
+ },
55400
56880
  {
55401
56881
  key: "react-doctor/no-indeterminate-attribute",
55402
56882
  id: "no-indeterminate-attribute",
@@ -55813,6 +57293,18 @@ const reactDoctorRules = [
55813
57293
  requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
55814
57294
  }
55815
57295
  },
57296
+ {
57297
+ key: "react-doctor/no-prop-callback-in-render",
57298
+ id: "no-prop-callback-in-render",
57299
+ source: "react-doctor",
57300
+ originallyExternal: false,
57301
+ rule: {
57302
+ ...noPropCallbackInRender,
57303
+ framework: "global",
57304
+ category: "Bugs",
57305
+ requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
57306
+ }
57307
+ },
55816
57308
  {
55817
57309
  key: "react-doctor/no-prop-types",
55818
57310
  id: "no-prop-types",
@@ -55904,6 +57396,18 @@ const reactDoctorRules = [
55904
57396
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
55905
57397
  }
55906
57398
  },
57399
+ {
57400
+ key: "react-doctor/no-ref-current-in-render",
57401
+ id: "no-ref-current-in-render",
57402
+ source: "react-doctor",
57403
+ originallyExternal: false,
57404
+ rule: {
57405
+ ...noRefCurrentInRender,
57406
+ framework: "global",
57407
+ category: "Bugs",
57408
+ requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
57409
+ }
57410
+ },
55907
57411
  {
55908
57412
  key: "react-doctor/no-render-in-render",
55909
57413
  id: "no-render-in-render",
@@ -58496,6 +60000,11 @@ const CROSS_FILE_RULE_IDS = new Set([
58496
60000
  "no-indeterminate-attribute",
58497
60001
  "no-locale-format-in-render",
58498
60002
  "no-match-media-in-state-initializer",
60003
+ "no-adjust-state-on-prop-change",
60004
+ "no-derived-state",
60005
+ "no-derived-state-effect",
60006
+ "no-event-handler",
60007
+ "no-initialize-state",
58499
60008
  "no-mutating-reducer-state",
58500
60009
  "no-unguarded-browser-global-in-render-or-hook-init",
58501
60010
  "prefer-dynamic-import",
@@ -58549,6 +60058,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
58549
60058
  if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
58550
60059
  for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
58551
60060
  };
60061
+ const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
60062
+ for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
60063
+ };
58552
60064
  const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
58553
60065
  const namedUseReducerLocals = /* @__PURE__ */ new Set();
58554
60066
  const reactObjectLocals = /* @__PURE__ */ new Set();
@@ -58635,6 +60147,11 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
58635
60147
  ["no-indeterminate-attribute", collectNearestManifestDependencies],
58636
60148
  ["no-locale-format-in-render", collectNearestManifestDependencies],
58637
60149
  ["no-match-media-in-state-initializer", collectNearestManifestDependencies],
60150
+ ["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
60151
+ ["no-derived-state", collectEffectValueHelperDependencies],
60152
+ ["no-derived-state-effect", collectEffectValueHelperDependencies],
60153
+ ["no-event-handler", collectEffectValueHelperDependencies],
60154
+ ["no-initialize-state", collectEffectValueHelperDependencies],
58638
60155
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
58639
60156
  ["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
58640
60157
  ["prefer-dynamic-import", collectNearestManifestDependencies],