oxlint-plugin-react-doctor 0.7.4-dev.70b5d99 → 0.7.4-dev.7bbb792

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 +185 -1
  2. package/dist/index.js +2755 -920
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1915,7 +1915,10 @@ const isGeneratedImageRenderContext = (context, node) => {
1915
1915
  if (!node) return false;
1916
1916
  const programRoot = findProgramRoot(node);
1917
1917
  if (!programRoot) return false;
1918
- return collectGeneratedImageJsxNodes(programRoot).has(node);
1918
+ const generatedImageJsxNodes = collectGeneratedImageJsxNodes(programRoot);
1919
+ if (generatedImageJsxNodes.has(node)) return true;
1920
+ if (isNodeOfType(node, "JSXElement")) return generatedImageJsxNodes.has(node.openingElement);
1921
+ return false;
1919
1922
  };
1920
1923
  //#endregion
1921
1924
  //#region src/plugin/utils/object-has-accessible-child.ts
@@ -8403,6 +8406,54 @@ const effectListenerCleanupMismatch = defineRule({
8403
8406
  } })
8404
8407
  });
8405
8408
  //#endregion
8409
+ //#region src/plugin/utils/collect-effect-invoked-functions.ts
8410
+ const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
8411
+ "then",
8412
+ "catch",
8413
+ "finally"
8414
+ ]);
8415
+ const collectEffectInvokedFunctions = (effectCallback) => {
8416
+ const invokedFunctions = new Set([effectCallback]);
8417
+ const localFunctionBindings = /* @__PURE__ */ new Map();
8418
+ const calledBindingNames = /* @__PURE__ */ new Set();
8419
+ const pendingFunctions = [effectCallback];
8420
+ const enqueue = (candidate) => {
8421
+ const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
8422
+ if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
8423
+ invokedFunctions.add(strippedCandidate);
8424
+ pendingFunctions.push(strippedCandidate);
8425
+ };
8426
+ const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
8427
+ while (pendingFunctions.length > 0) {
8428
+ const currentFunction = pendingFunctions.pop();
8429
+ if (!currentFunction) break;
8430
+ walkAst(currentFunction, (child) => {
8431
+ if (child !== currentFunction && isFunctionLike$1(child)) {
8432
+ if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
8433
+ return false;
8434
+ }
8435
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
8436
+ const initializer = child.init ? stripParenExpression(child.init) : null;
8437
+ if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
8438
+ return;
8439
+ }
8440
+ if (!isNodeOfType(child, "CallExpression")) return;
8441
+ const callee = stripParenExpression(child.callee);
8442
+ if (isFunctionLike$1(callee)) {
8443
+ enqueue(callee);
8444
+ return;
8445
+ }
8446
+ if (isNodeOfType(callee, "Identifier")) {
8447
+ calledBindingNames.add(callee.name);
8448
+ return;
8449
+ }
8450
+ if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
8451
+ });
8452
+ for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
8453
+ }
8454
+ return invokedFunctions;
8455
+ };
8456
+ //#endregion
8406
8457
  //#region src/plugin/utils/is-react-hook-name.ts
8407
8458
  const isReactHookName = (name) => {
8408
8459
  if (!name.startsWith("use")) return false;
@@ -8458,46 +8509,90 @@ const enclosingComponentOrHookName = (node) => {
8458
8509
  return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
8459
8510
  };
8460
8511
  //#endregion
8512
+ //#region src/plugin/utils/find-declarator-for-binding.ts
8513
+ const findDeclaratorForBinding = (bindingIdentifier) => {
8514
+ let ancestor = bindingIdentifier.parent;
8515
+ while (ancestor) {
8516
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
8517
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
8518
+ ancestor = ancestor.parent ?? null;
8519
+ }
8520
+ return null;
8521
+ };
8522
+ //#endregion
8523
+ //#region src/plugin/utils/executes-during-render.ts
8524
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
8525
+ "map",
8526
+ "filter",
8527
+ "forEach",
8528
+ "flatMap",
8529
+ "reduce",
8530
+ "reduceRight",
8531
+ "some",
8532
+ "every",
8533
+ "find",
8534
+ "findIndex",
8535
+ "findLast",
8536
+ "findLastIndex",
8537
+ "sort",
8538
+ "toSorted"
8539
+ ]);
8540
+ const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
8541
+ const isGlobalArrayFromMember = (node, scopes) => {
8542
+ const unwrappedNode = stripParenExpression(node);
8543
+ if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
8544
+ return unwrappedNode.property.name === "from";
8545
+ };
8546
+ const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
8547
+ const unwrappedNode = stripParenExpression(node);
8548
+ if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
8549
+ const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
8550
+ if (!binding?.initializer) return false;
8551
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
8552
+ return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
8553
+ };
8554
+ const executesDuringRender = (functionNode, scopes) => {
8555
+ const parent = functionNode.parent;
8556
+ if (!isNodeOfType(parent, "CallExpression")) return false;
8557
+ if (parent.callee === functionNode) return true;
8558
+ 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;
8559
+ if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
8560
+ 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;
8561
+ };
8562
+ //#endregion
8563
+ //#region src/plugin/utils/find-render-phase-component-or-hook.ts
8564
+ const findRenderPhaseComponentOrHook = (node, scopes) => {
8565
+ let functionNode = findEnclosingFunction(node);
8566
+ while (functionNode) {
8567
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
8568
+ if (!executesDuringRender(functionNode, scopes)) return null;
8569
+ functionNode = findEnclosingFunction(functionNode);
8570
+ }
8571
+ return null;
8572
+ };
8573
+ //#endregion
8574
+ //#region src/plugin/utils/get-function-binding-name.ts
8575
+ const getFunctionBindingIdentifier = (functionNode) => {
8576
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
8577
+ const parent = functionNode.parent;
8578
+ if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
8579
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
8580
+ if (isNodeOfType(parent, "CallExpression")) {
8581
+ const callParent = parent.parent;
8582
+ if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
8583
+ }
8584
+ return null;
8585
+ };
8586
+ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
8587
+ //#endregion
8461
8588
  //#region src/plugin/utils/get-range-start.ts
8462
8589
  const getRangeStart = (node) => {
8463
8590
  const rangeStart = node.range?.[0];
8464
8591
  return typeof rangeStart === "number" ? rangeStart : null;
8465
8592
  };
8466
8593
  //#endregion
8467
- //#region src/plugin/utils/is-result-discarded-call.ts
8468
- const isResultDiscardedCall = (callExpression) => {
8469
- let node = callExpression;
8470
- let parent = node.parent;
8471
- while (parent) {
8472
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
8473
- if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
8474
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
8475
- if (isNodeOfType(parent, "ChainExpression")) {
8476
- node = parent;
8477
- parent = node.parent;
8478
- continue;
8479
- }
8480
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
8481
- node = parent;
8482
- parent = node.parent;
8483
- continue;
8484
- }
8485
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
8486
- node = parent;
8487
- parent = node.parent;
8488
- continue;
8489
- }
8490
- if (isNodeOfType(parent, "SequenceExpression")) {
8491
- const expressions = parent.expressions ?? [];
8492
- if (expressions[expressions.length - 1] !== node) return true;
8493
- node = parent;
8494
- parent = node.parent;
8495
- continue;
8496
- }
8497
- return false;
8498
- }
8499
- return false;
8500
- };
8594
+ //#region src/plugin/utils/is-event-handler-attribute.ts
8595
+ const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
8501
8596
  //#endregion
8502
8597
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
8503
8598
  const walkInsideStatementBlocks = (node, visitor) => {
@@ -8532,125 +8627,9 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
8532
8627
  return true;
8533
8628
  };
8534
8629
  //#endregion
8535
- //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
8536
- const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
8537
- "each",
8538
- "every",
8539
- "filter",
8540
- "find",
8541
- "findIndex",
8542
- "findLast",
8543
- "findLastIndex",
8544
- "flatMap",
8545
- "forEach",
8546
- "map",
8547
- "reduce",
8548
- "reduceRight",
8549
- "some",
8550
- "sort",
8551
- "toSorted"
8552
- ]);
8553
- const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
8554
- "from",
8555
- "fromAsync",
8556
- "groupBy"
8557
- ]);
8558
- const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
8559
- const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
8560
- const isListenerRemovalViaNullHandler = (callNode) => {
8561
- if (!isNodeOfType(callNode, "CallExpression")) return false;
8562
- const callee = unwrapChainExpression$3(callNode.callee);
8563
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
8564
- };
8565
- const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
8566
- "off",
8567
- "removeEventListener",
8568
- "removeListener"
8569
- ]);
8570
- const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
8571
- if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
8572
- const handlerArgument = callNode.arguments?.[1];
8573
- return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
8574
- };
8575
- const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8576
- const callNode = unwrapChainExpression$3(node);
8577
- if (!isNodeOfType(callNode, "CallExpression")) return false;
8578
- if (isListenerRemovalViaNullHandler(callNode)) return true;
8579
- const callee = unwrapChainExpression$3(callNode.callee);
8580
- if (isNodeOfType(callee, "Identifier")) {
8581
- if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
8582
- if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
8583
- if (knownCleanupFunctionNames.has(callee.name)) return true;
8584
- return false;
8585
- }
8586
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
8587
- if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
8588
- if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
8589
- return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
8590
- }
8591
- return false;
8592
- };
8593
- 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);
8594
- const isIteratorCallbackArgument = (node) => {
8595
- const parentNode = node.parent;
8596
- if (!isNodeOfType(parentNode, "CallExpression")) return false;
8597
- if (!parentNode.arguments?.some((argument) => argument === node)) return false;
8598
- const callee = unwrapChainExpression$3(parentNode.callee);
8599
- if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
8600
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
8601
- };
8602
- const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8603
- let didFindRelease = false;
8604
- walkAst(node, (child) => {
8605
- if (didFindRelease) return false;
8606
- if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
8607
- if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
8608
- didFindRelease = true;
8609
- return false;
8610
- }
8611
- });
8612
- return didFindRelease;
8613
- };
8614
- const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8615
- if (!isFunctionLike$1(node)) return false;
8616
- return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
8617
- };
8618
- const isProvablyNoOpCleanupFunction = (node) => {
8619
- if (!isFunctionLike$1(node)) return false;
8620
- let sawNoOpRemoval = false;
8621
- let sawOtherCall = false;
8622
- walkAst(node.body, (child) => {
8623
- if (sawOtherCall) return false;
8624
- if (isFunctionLike$1(child)) return false;
8625
- const callNode = unwrapChainExpression$3(child);
8626
- if (!isNodeOfType(callNode, "CallExpression")) return;
8627
- const callee = unwrapChainExpression$3(callNode.callee);
8628
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
8629
- sawNoOpRemoval = true;
8630
- return;
8631
- }
8632
- sawOtherCall = true;
8633
- });
8634
- return sawNoOpRemoval && !sawOtherCall;
8635
- };
8636
- const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
8637
- if (!returnedValue) return false;
8638
- const unwrappedValue = unwrapChainExpression$3(returnedValue);
8639
- if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
8640
- if (isNodeOfType(unwrappedValue, "Identifier")) {
8641
- if (unwrappedValue.name === "undefined") return false;
8642
- if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
8643
- return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
8644
- }
8645
- if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
8646
- if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
8647
- if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
8648
- if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
8649
- return false;
8650
- };
8651
- //#endregion
8652
8630
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
8653
8631
  const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
8632
+ const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
8654
8633
  const RESOURCE_NOUN_BY_KIND = {
8655
8634
  subscribe: "subscription",
8656
8635
  timer: "timer",
@@ -8661,7 +8640,66 @@ const isSubscribeOrObserveCall = (node) => {
8661
8640
  if (isSubscribeLikeCallExpression(node)) return true;
8662
8641
  return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
8663
8642
  };
8664
- const findSubscribeLikeUsages = (callback) => {
8643
+ const resolveExpressionKey$1 = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
8644
+ if (!expression) return null;
8645
+ const unwrappedExpression = stripParenExpression(expression);
8646
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
8647
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
8648
+ if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
8649
+ if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
8650
+ visitedSymbolIds.add(symbol.id);
8651
+ const bindingProperty = symbol.bindingIdentifier.parent;
8652
+ const bindingPattern = bindingProperty?.parent;
8653
+ const variableDeclarator = bindingPattern?.parent;
8654
+ const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
8655
+ if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
8656
+ const objectKey = resolveExpressionKey$1(variableDeclarator.init, context, visitedSymbolIds);
8657
+ return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
8658
+ }
8659
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
8660
+ if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey$1(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
8661
+ return `symbol:${symbol.id}`;
8662
+ }
8663
+ if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
8664
+ if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
8665
+ const objectKey = resolveExpressionKey$1(unwrappedExpression.object, context, visitedSymbolIds);
8666
+ return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
8667
+ }
8668
+ if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
8669
+ if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
8670
+ if (isFunctionLike$1(unwrappedExpression)) {
8671
+ const rangeStart = getRangeStart(unwrappedExpression);
8672
+ return rangeStart === null ? null : `function:${rangeStart}`;
8673
+ }
8674
+ return null;
8675
+ };
8676
+ const findAssignedResourceKey = (resourceNode, context) => {
8677
+ let currentNode = resourceNode;
8678
+ let parentNode = currentNode.parent;
8679
+ while (isNodeOfType(parentNode, "ChainExpression")) {
8680
+ currentNode = parentNode;
8681
+ parentNode = currentNode.parent;
8682
+ }
8683
+ if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
8684
+ if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey$1(parentNode.left, context);
8685
+ return null;
8686
+ };
8687
+ const getCallRegistrationDetails = (callNode, context) => {
8688
+ const callee = stripParenExpression(callNode.callee);
8689
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return {
8690
+ receiverKey: null,
8691
+ registrationVerbName: null,
8692
+ eventKey: null,
8693
+ handlerKey: null
8694
+ };
8695
+ return {
8696
+ receiverKey: resolveExpressionKey$1(callee.object, context),
8697
+ registrationVerbName: callee.property.name,
8698
+ eventKey: resolveExpressionKey$1(callNode.arguments?.[0], context),
8699
+ handlerKey: resolveExpressionKey$1(callNode.arguments?.[1], context)
8700
+ };
8701
+ };
8702
+ const findSubscribeLikeUsages = (callback, context) => {
8665
8703
  const usages = [];
8666
8704
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8667
8705
  let cleanupArgument = null;
@@ -8670,13 +8708,22 @@ const findSubscribeLikeUsages = (callback) => {
8670
8708
  const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
8671
8709
  if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
8672
8710
  }
8711
+ const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
8673
8712
  walkAst(callback, (child) => {
8674
- if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
8713
+ if (child !== callback && isFunctionLike$1(child)) {
8714
+ if (child === cleanupArgument) return false;
8715
+ if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
8716
+ }
8675
8717
  if (isSocketConstruction(child)) {
8676
8718
  usages.push({
8677
8719
  kind: "socket",
8678
8720
  node: child,
8679
- resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8721
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
8722
+ handleKey: findAssignedResourceKey(child, context),
8723
+ receiverKey: null,
8724
+ registrationVerbName: null,
8725
+ eventKey: null,
8726
+ handlerKey: null
8680
8727
  });
8681
8728
  return;
8682
8729
  }
@@ -8685,118 +8732,348 @@ const findSubscribeLikeUsages = (callback) => {
8685
8732
  usages.push({
8686
8733
  kind: "timer",
8687
8734
  node: child,
8688
- resourceName: child.callee.name
8735
+ resourceName: child.callee.name,
8736
+ handleKey: findAssignedResourceKey(child, context),
8737
+ receiverKey: null,
8738
+ registrationVerbName: child.callee.name,
8739
+ eventKey: null,
8740
+ handlerKey: null
8689
8741
  });
8690
8742
  return;
8691
8743
  }
8692
- 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({
8693
- kind: "subscribe",
8694
- node: child,
8695
- resourceName: child.callee.property.name
8696
- });
8697
- });
8698
- return usages;
8699
- };
8700
- const collectCleanupBindings = (effectCallback) => {
8701
- const bindings = {
8702
- cleanupFunctionNames: /* @__PURE__ */ new Set(),
8703
- subscriptionNames: /* @__PURE__ */ new Set(),
8704
- effectScopeVariableNames: /* @__PURE__ */ new Set()
8705
- };
8706
- if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
8707
- if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
8708
- walkAst(effectCallback.body, (child) => {
8709
- if (!isSubscribeOrObserveCall(child)) return;
8710
- if (!isNodeOfType(child, "CallExpression")) return;
8711
- if (!isNodeOfType(child.callee, "MemberExpression")) return;
8712
- if (!isNodeOfType(child.callee.object, "Identifier")) return;
8713
- bindings.subscriptionNames.add(child.callee.object.name);
8714
- });
8715
- walkInsideStatementBlocks(effectCallback.body, (child) => {
8716
- if (!isNodeOfType(child, "VariableDeclaration")) return;
8717
- for (const declarator of child.declarations ?? []) {
8718
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
8719
- const bindingName = declarator.id.name;
8720
- bindings.effectScopeVariableNames.add(bindingName);
8721
- const init = declarator.init;
8722
- if (!init) continue;
8723
- if (isSocketConstruction(init)) {
8724
- bindings.subscriptionNames.add(bindingName);
8725
- continue;
8726
- }
8727
- if (!isNodeOfType(init, "CallExpression")) continue;
8728
- if (isSubscribeLikeCallExpression(init)) {
8729
- bindings.subscriptionNames.add(bindingName);
8730
- if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
8731
- }
8732
- }
8733
- });
8734
- walkAst(effectCallback.body, (child) => {
8735
- if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
8736
- if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
8737
- bindings.cleanupFunctionNames.add(child.id.name);
8738
- return false;
8739
- }
8740
- });
8741
- walkInsideStatementBlocks(effectCallback.body, (child) => {
8742
- if (!isNodeOfType(child, "VariableDeclaration")) return;
8743
- for (const declarator of child.declarations ?? []) {
8744
- if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
8745
- if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
8744
+ 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)) {
8745
+ const registrationDetails = getCallRegistrationDetails(child, context);
8746
+ usages.push({
8747
+ kind: "subscribe",
8748
+ node: child,
8749
+ resourceName: child.callee.property.name,
8750
+ handleKey: findAssignedResourceKey(child, context),
8751
+ ...registrationDetails
8752
+ });
8746
8753
  }
8747
8754
  });
8748
- walkAst(effectCallback.body, (child) => {
8749
- 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);
8750
- });
8751
- return bindings;
8752
- };
8753
- const removeSynchronouslyReleasedUsages = (callback, usages) => {
8755
+ return usages.filter((usage) => isNodeReachableWithinFunction(usage.node, context));
8756
+ };
8757
+ const isNodeReachableWithinFunction = (node, context) => {
8758
+ const owner = context.cfg.enclosingFunction(node);
8759
+ if (!owner) return true;
8760
+ const functionCfg = context.cfg.cfgFor(owner);
8761
+ if (!functionCfg) return true;
8762
+ const targetBlock = functionCfg.blockOf(node);
8763
+ if (!targetBlock) return true;
8764
+ const visitedBlocks = new Set([functionCfg.entry]);
8765
+ const pendingBlocks = [functionCfg.entry];
8766
+ while (pendingBlocks.length > 0) {
8767
+ const currentBlock = pendingBlocks.pop();
8768
+ if (!currentBlock) break;
8769
+ if (currentBlock === targetBlock) return true;
8770
+ for (const edge of currentBlock.successors) {
8771
+ if (visitedBlocks.has(edge.to)) continue;
8772
+ visitedBlocks.add(edge.to);
8773
+ pendingBlocks.push(edge.to);
8774
+ }
8775
+ }
8776
+ return false;
8777
+ };
8778
+ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, context) => {
8779
+ let pathAnchor = usageNode;
8780
+ let pathOwner = findEnclosingFunction(pathAnchor);
8781
+ while (pathOwner && isSynchronousIteratorCallback(pathOwner)) {
8782
+ if (matchingNodes.length > 0 && matchingNodes.every((matchingNode) => context.cfg.enclosingFunction(matchingNode) === pathOwner)) break;
8783
+ const iteratorCall = pathOwner.parent;
8784
+ if (!isNodeOfType(iteratorCall, "CallExpression")) break;
8785
+ pathAnchor = iteratorCall;
8786
+ pathOwner = findEnclosingFunction(pathAnchor);
8787
+ }
8788
+ const owner = context.cfg.enclosingFunction(pathAnchor);
8789
+ if (!owner) return false;
8790
+ const functionCfg = context.cfg.cfgFor(owner);
8791
+ if (!functionCfg) return false;
8792
+ const usageBlock = functionCfg.blockOf(pathAnchor);
8793
+ if (!usageBlock) return false;
8794
+ const usageStart = getRangeStart(usageNode);
8795
+ const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
8796
+ if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
8797
+ const matchingBlock = functionCfg.blockOf(matchingNode);
8798
+ if (!matchingBlock) return [];
8799
+ const matchingStart = getRangeStart(matchingNode);
8800
+ if (matchingBlock === usageBlock && usageStart !== null && matchingStart !== null && matchingStart < usageStart) return [];
8801
+ return [matchingBlock];
8802
+ }));
8803
+ if (matchingBlocks.has(usageBlock)) return true;
8804
+ const visitedBlocks = new Set([usageBlock]);
8805
+ const pendingBlocks = [usageBlock];
8806
+ while (pendingBlocks.length > 0) {
8807
+ const currentBlock = pendingBlocks.pop();
8808
+ if (!currentBlock) break;
8809
+ for (const edge of currentBlock.successors) {
8810
+ if (matchingBlocks.has(edge.to)) continue;
8811
+ if (edge.to === functionCfg.exit) return false;
8812
+ if (visitedBlocks.has(edge.to)) continue;
8813
+ visitedBlocks.add(edge.to);
8814
+ pendingBlocks.push(edge.to);
8815
+ }
8816
+ }
8817
+ return matchingBlocks.size > 0;
8818
+ };
8819
+ const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
8754
8820
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8755
8821
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8756
- const releaseStarts = [];
8822
+ const releaseCalls = [];
8757
8823
  walkInsideStatementBlocks(callback.body, (child) => {
8758
- if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8759
- const releaseStart = getRangeStart(child);
8760
- if (releaseStart !== null) releaseStarts.push(releaseStart);
8824
+ if (!isNodeOfType(isNodeOfType(child, "ChainExpression") ? child.expression : child, "CallExpression")) return;
8825
+ releaseCalls.push(child);
8761
8826
  });
8762
- if (releaseStarts.length === 0) return usages;
8827
+ if (releaseCalls.length === 0) return usages;
8763
8828
  return usages.filter((usage) => {
8764
8829
  const usageStart = getRangeStart(usage.node);
8765
8830
  if (usageStart === null) return true;
8766
- return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8831
+ const matchingReleaseCalls = releaseCalls.filter((releaseCall) => {
8832
+ const releaseStart = getRangeStart(releaseCall);
8833
+ return releaseStart !== null && releaseStart > usageStart && doesReleaseCallMatchUsage(releaseCall, usage, context);
8834
+ });
8835
+ return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
8767
8836
  });
8768
8837
  };
8769
- const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
8770
- if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
8771
- const returnStart = getRangeStart(returnStatement);
8772
- if (returnStart === null) return true;
8773
- return usages.some((usage) => {
8774
- const usageStart = getRangeStart(usage.node);
8775
- return usageStart === null || usageStart < returnStart;
8838
+ const resolveIteratorCollectionKey = (expression, context) => {
8839
+ if (!expression || !isNodeOfType(expression, "Identifier")) return null;
8840
+ const symbol = context.scopes.symbolFor(expression);
8841
+ if (!symbol || symbol.kind !== "parameter") return null;
8842
+ let callbackNode = symbol.bindingIdentifier.parent;
8843
+ while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
8844
+ if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
8845
+ const callNode = callbackNode.parent;
8846
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8847
+ const callee = stripParenExpression(callNode.callee);
8848
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
8849
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return resolveExpressionKey$1(callNode.arguments[0], context);
8850
+ if (callNode.arguments?.[0] === callbackNode) return resolveExpressionKey$1(callee.object, context);
8851
+ }
8852
+ return null;
8853
+ };
8854
+ const findCollectionMappingCall = (callbackNode) => {
8855
+ if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
8856
+ const callNode = callbackNode.parent;
8857
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8858
+ const callee = stripParenExpression(callNode.callee);
8859
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return null;
8860
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from" && callNode.arguments?.[1] === callbackNode) return callNode;
8861
+ return callee.property.name === "map" && callNode.arguments?.[0] === callbackNode ? callNode : null;
8862
+ };
8863
+ const findMappedResourceCollectionKey = (resourceNode, context) => {
8864
+ const callbackNode = findEnclosingFunction(resourceNode);
8865
+ if (!callbackNode || !isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression")) return null;
8866
+ const mappingCall = findCollectionMappingCall(callbackNode);
8867
+ if (!mappingCall) return null;
8868
+ if (isNodeOfType(callbackNode.body, "BlockStatement")) {
8869
+ const resourceRoot = findTransparentExpressionRoot(resourceNode);
8870
+ const resourceDeclarator = resourceRoot.parent;
8871
+ const resourceDeclaration = resourceDeclarator?.parent;
8872
+ if (!isNodeOfType(resourceDeclarator, "VariableDeclarator") || resourceDeclarator.init !== resourceRoot || !isNodeOfType(resourceDeclarator.id, "Identifier") || !isNodeOfType(resourceDeclaration, "VariableDeclaration") || resourceDeclaration.kind !== "const" || resourceDeclaration.parent !== callbackNode.body) return null;
8873
+ const returnStatements = [];
8874
+ walkAst(callbackNode.body, (child) => {
8875
+ if (child !== callbackNode.body && isFunctionLike$1(child)) return false;
8876
+ if (isNodeOfType(child, "ReturnStatement")) returnStatements.push(child);
8877
+ });
8878
+ const returnStatement = returnStatements[0];
8879
+ const callbackStatements = callbackNode.body.body ?? [];
8880
+ const returnedIdentifier = isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument ? stripParenExpression(returnStatement.argument) : null;
8881
+ const resourceSymbol = context.scopes.symbolFor(resourceDeclarator.id);
8882
+ 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;
8883
+ } else if (findTransparentExpressionRoot(resourceNode) !== callbackNode.body) return null;
8884
+ const mappingRoot = findTransparentExpressionRoot(mappingCall);
8885
+ const collectionDeclarator = mappingRoot.parent;
8886
+ return isNodeOfType(collectionDeclarator, "VariableDeclarator") && collectionDeclarator.init === mappingRoot ? resolveExpressionKey$1(collectionDeclarator.id, context) : null;
8887
+ };
8888
+ const findContainingCollectionKey = (resourceNode, context) => {
8889
+ const mappedCollectionKey = findMappedResourceCollectionKey(resourceNode, context);
8890
+ if (mappedCollectionKey !== null) return mappedCollectionKey;
8891
+ let currentNode = resourceNode;
8892
+ let parentNode = currentNode.parent;
8893
+ while (parentNode) {
8894
+ if (isFunctionLike$1(parentNode)) return null;
8895
+ if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey$1(parentNode.id, context);
8896
+ currentNode = parentNode;
8897
+ parentNode = currentNode.parent;
8898
+ }
8899
+ return null;
8900
+ };
8901
+ const isWithinAssignmentTarget = (identifier) => {
8902
+ let currentNode = identifier;
8903
+ let parentNode = currentNode.parent;
8904
+ while (parentNode) {
8905
+ if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
8906
+ if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
8907
+ if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
8908
+ currentNode = parentNode;
8909
+ parentNode = currentNode.parent;
8910
+ }
8911
+ return false;
8912
+ };
8913
+ const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
8914
+ if (!expression) return null;
8915
+ const unwrappedExpression = stripParenExpression(expression);
8916
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
8917
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
8918
+ 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;
8919
+ 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;
8920
+ 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;
8921
+ if (!symbol || symbol.kind !== "const" && !isUnreassignedMutableBinding && !isUnreassignedFunctionBinding || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
8922
+ visitedSymbolIds.add(symbol.id);
8923
+ return resolveStableValue(symbol.initializer, context, visitedSymbolIds);
8924
+ };
8925
+ const resolveObjectExpression = (expression, context) => {
8926
+ const resolvedExpression = resolveStableValue(expression, context);
8927
+ return isNodeOfType(resolvedExpression, "ObjectExpression") ? resolvedExpression : null;
8928
+ };
8929
+ const getListenerAbortControllerKey = (usage, context) => {
8930
+ if (usage.registrationVerbName !== "addEventListener" || !isNodeOfType(usage.node, "CallExpression")) return null;
8931
+ const optionsArgument = usage.node.arguments?.[2];
8932
+ const optionsObject = resolveObjectExpression(optionsArgument, context);
8933
+ if (!optionsObject) return null;
8934
+ for (const property of optionsObject.properties ?? []) {
8935
+ if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== "signal") continue;
8936
+ const signalKey = resolveExpressionKey$1(property.value, context);
8937
+ return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
8938
+ }
8939
+ return null;
8940
+ };
8941
+ const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
8942
+ "every",
8943
+ "filter",
8944
+ "flatMap",
8945
+ "forEach",
8946
+ "map",
8947
+ "reduce",
8948
+ "reduceRight",
8949
+ "some"
8950
+ ]);
8951
+ const isSynchronousIteratorCallback = (functionNode) => {
8952
+ const callNode = functionNode.parent;
8953
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
8954
+ const callee = stripParenExpression(callNode.callee);
8955
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
8956
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
8957
+ return SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
8958
+ };
8959
+ const findDirectCallForReference = (identifier) => {
8960
+ const expressionRoot = findTransparentExpressionRoot(identifier);
8961
+ const callNode = expressionRoot.parent;
8962
+ return isNodeOfType(callNode, "CallExpression") && callNode.callee === expressionRoot ? callNode : null;
8963
+ };
8964
+ const findSingleDirectInvocation = (functionNode, caller, context) => {
8965
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
8966
+ if (!bindingIdentifier || resolveStableValue(bindingIdentifier, context) !== functionNode) return null;
8967
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
8968
+ if (!symbol) return null;
8969
+ const invocationCalls = symbol.references.flatMap((reference) => {
8970
+ const callNode = findDirectCallForReference(reference.identifier);
8971
+ return callNode ? [callNode] : [];
8972
+ });
8973
+ if (invocationCalls.length !== 1) return null;
8974
+ const invocationCall = invocationCalls[0];
8975
+ return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
8976
+ };
8977
+ const resolveCleanupPathAnchor = (usageNode, effectCallback, context) => {
8978
+ const usageFunction = findEnclosingFunction(usageNode);
8979
+ if (!usageFunction || usageFunction === effectCallback) return usageNode;
8980
+ return findSingleDirectInvocation(usageFunction, effectCallback, context) ?? usageNode;
8981
+ };
8982
+ const resolveSingleAssignedCleanupFunction = (expression, usage, context) => {
8983
+ const unwrappedExpression = stripParenExpression(expression);
8984
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
8985
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
8986
+ const initializer = symbol?.initializer ? stripParenExpression(symbol.initializer) : null;
8987
+ 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;
8988
+ const assignmentReferences = symbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
8989
+ if (assignmentReferences.length !== 1) return null;
8990
+ const assignmentReference = assignmentReferences[0];
8991
+ const assignmentTarget = findTransparentExpressionRoot(assignmentReference.identifier);
8992
+ const assignmentNode = assignmentTarget.parent;
8993
+ if (!isNodeOfType(assignmentNode, "AssignmentExpression") || assignmentNode.operator !== "=" || assignmentNode.left !== assignmentTarget || findEnclosingFunction(assignmentNode) !== findEnclosingFunction(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [assignmentNode], context)) return null;
8994
+ const assignedValue = stripParenExpression(assignmentNode.right);
8995
+ return isFunctionLike$1(assignedValue) ? assignedValue : null;
8996
+ };
8997
+ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visitedFunctions = /* @__PURE__ */ new Set()) => {
8998
+ if (!isFunctionLike$1(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
8999
+ visitedFunctions.add(cleanupFunction);
9000
+ let didCleanupFunctionMatch = false;
9001
+ walkAst(cleanupFunction.body, (cleanupChild) => {
9002
+ if (didCleanupFunctionMatch) return false;
9003
+ if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
9004
+ if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
9005
+ didCleanupFunctionMatch = true;
9006
+ return false;
9007
+ }
9008
+ const helperCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
9009
+ if (!isNodeOfType(helperCall, "CallExpression")) return;
9010
+ const stableHelperFunction = resolveStableValue(helperCall.callee, context);
9011
+ const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
9012
+ if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions)) {
9013
+ didCleanupFunctionMatch = true;
9014
+ return false;
9015
+ }
8776
9016
  });
9017
+ return didCleanupFunctionMatch;
8777
9018
  };
8778
- const effectHasCleanupReturn = (callback, usages) => {
9019
+ const effectHasCleanupForUsage = (callback, usage, context) => {
8779
9020
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
8780
- if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
8781
- const cleanupBindings = collectCleanupBindings(callback);
8782
- let didFindCleanupReturn = false;
9021
+ if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
9022
+ if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
9023
+ const matchingCleanupReturns = [];
8783
9024
  walkInsideStatementBlocks(callback.body, (child) => {
8784
- if (didFindCleanupReturn) return;
8785
9025
  if (!isNodeOfType(child, "ReturnStatement")) return;
8786
- if (!cleanupReturnRunsAfterUsage(child, usages)) return;
8787
- if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
9026
+ const returnStart = getRangeStart(child);
9027
+ const usageStart = getRangeStart(usage.node);
9028
+ if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9029
+ const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9030
+ if (!returnedValue) return;
9031
+ if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9032
+ matchingCleanupReturns.push(child);
9033
+ return;
9034
+ }
9035
+ if (usage.kind === "subscribe" && isNodeOfType(returnedValue, "Identifier") && usage.handleKey !== null && resolveExpressionKey$1(returnedValue, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) {
9036
+ matchingCleanupReturns.push(child);
9037
+ return;
9038
+ }
9039
+ if (isNodeOfType(returnedValue, "Identifier")) {
9040
+ if (returnedValue.name === "undefined" && context.scopes.isGlobalReference(returnedValue)) return;
9041
+ const returnedKey = resolveExpressionKey$1(returnedValue, context);
9042
+ if (usage.handleKey !== null && returnedKey === usage.handleKey) return;
9043
+ if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9044
+ }
9045
+ const cleanupFunction = resolveStableValue(returnedValue, context);
9046
+ if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9047
+ if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
8788
9048
  });
8789
- return didFindCleanupReturn;
9049
+ return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9050
+ };
9051
+ const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9052
+ for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
9053
+ return null;
9054
+ };
9055
+ const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
9056
+ const unwrappedExpression = stripParenExpression(expression);
9057
+ if (isNodeOfType(unwrappedExpression, "NewExpression") && isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "AbortController") return true;
9058
+ if (isNodeOfType(unwrappedExpression, "MemberExpression")) return isLocalAbortControllerExpression(unwrappedExpression.object, context, visitedSymbolIds);
9059
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return false;
9060
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
9061
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
9062
+ visitedSymbolIds.add(symbol.id);
9063
+ if (symbol.initializer) return isLocalAbortControllerExpression(symbol.initializer, context, visitedSymbolIds);
9064
+ const bindingProperty = symbol.bindingIdentifier.parent;
9065
+ const bindingPattern = bindingProperty?.parent;
9066
+ const variableDeclarator = bindingPattern?.parent;
9067
+ return Boolean(isNodeOfType(bindingProperty, "Property") && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.init && isLocalAbortControllerExpression(variableDeclarator.init, context, visitedSymbolIds));
8790
9068
  };
8791
- const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8792
- const isSelfReleasingListenerOptionProperty = (property) => {
9069
+ const isSelfReleasingListenerOptionProperty = (property, context) => {
8793
9070
  if (!isNodeOfType(property, "Property")) return false;
8794
9071
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8795
- if (keyName === "signal") return true;
9072
+ if (keyName === "signal") return !isLocalAbortControllerExpression(property.value, context);
8796
9073
  if (keyName !== "once") return false;
8797
9074
  return isNodeOfType(property.value, "Literal") && property.value.value === true;
8798
9075
  };
8799
- const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
9076
+ const hasSelfReleasingListenerOptions = (node, context) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isSelfReleasingListenerOptionProperty(property, context)));
8800
9077
  const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8801
9078
  ["addEventListener", new Set(["removeEventListener", "abort"])],
8802
9079
  ["addListener", new Set([
@@ -8822,85 +9099,185 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8822
9099
  "teardown"
8823
9100
  ]);
8824
9101
  const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8825
- const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8826
9102
  const getReleaseVerbName = (node) => {
8827
- if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8828
9103
  const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8829
9104
  if (!isNodeOfType(callNode, "CallExpression")) return null;
8830
9105
  const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8831
- if (isNodeOfType(callee, "Identifier")) return callee.name;
8832
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9106
+ 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;
9107
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
9108
+ const methodName = callee.property.name;
9109
+ return GLOBAL_RELEASE_METHOD_NAMES.has(methodName) || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(methodName) || methodName === "on" ? methodName : null;
9110
+ }
8833
9111
  return null;
8834
9112
  };
9113
+ const doesReleaseCallMatchUsage = (node, usage, context) => {
9114
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
9115
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
9116
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
9117
+ if (usage.kind === "timer") {
9118
+ const expectedCleanupName = usage.registrationVerbName === "setInterval" ? "clearInterval" : "clearTimeout";
9119
+ if (!isNodeOfType(callee, "Identifier") || !TIMER_CLEANUP_CALLEE_NAMES.has(callee.name) || callee.name !== expectedCleanupName) return false;
9120
+ if (usage.handleKey !== null && resolveExpressionKey$1(callNode.arguments?.[0], context) === usage.handleKey) return true;
9121
+ const collectionKey = findContainingCollectionKey(usage.node, context);
9122
+ return collectionKey !== null && collectionKey === resolveIteratorCollectionKey(callNode.arguments?.[0], context);
9123
+ }
9124
+ if (isNodeOfType(callee, "Identifier") && usage.kind === "subscribe" && usage.handleKey !== null && resolveExpressionKey$1(callee, context) === usage.handleKey && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
9125
+ const releaseVerbName = getReleaseVerbName(callNode);
9126
+ if (!releaseVerbName) return false;
9127
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
9128
+ const releaseReceiverKey = resolveExpressionKey$1(callee.object, context);
9129
+ if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
9130
+ 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;
9131
+ if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
9132
+ if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
9133
+ const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
9134
+ if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
9135
+ const releaseEventKey = resolveExpressionKey$1(callNode.arguments?.[0], context);
9136
+ if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
9137
+ const usageIteratorCollectionKey = isNodeOfType(usage.node, "CallExpression") ? resolveIteratorCollectionKey(usage.node.arguments?.[0], context) : null;
9138
+ const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
9139
+ if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
9140
+ }
9141
+ if (releaseVerbName === "on") {
9142
+ const handlerArgument = callNode.arguments?.[1];
9143
+ return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
9144
+ }
9145
+ if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
9146
+ const releaseHandler = callNode.arguments?.[1];
9147
+ if (!releaseHandler) return releaseVerbName === "off";
9148
+ return usage.handlerKey !== null && resolveExpressionKey$1(releaseHandler, context) === usage.handlerKey;
9149
+ }
9150
+ if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
9151
+ return true;
9152
+ };
8835
9153
  const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8836
- const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8837
- let didFindPairedRelease = false;
8838
- walkAst(body, (child) => {
8839
- if (didFindPairedRelease) return false;
8840
- if (child !== body && isFunctionLike$1(child)) return false;
8841
- const releaseVerbName = getReleaseVerbName(child);
8842
- if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8843
- didFindPairedRelease = true;
8844
- return false;
8845
- }
8846
- });
8847
- return didFindPairedRelease;
9154
+ const isReturnedEffectCleanupFunction = (functionNode) => {
9155
+ let currentNode = functionNode;
9156
+ let parentNode = currentNode.parent;
9157
+ while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
9158
+ currentNode = parentNode;
9159
+ parentNode = currentNode.parent;
9160
+ }
9161
+ if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
9162
+ const effectCallback = findEnclosingFunction(parentNode);
9163
+ const effectCall = effectCallback?.parent;
9164
+ return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
9165
+ };
9166
+ const isPotentiallyReachableFunction = (functionNode, context) => {
9167
+ if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
9168
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
9169
+ if (!bindingIdentifier) return false;
9170
+ const symbol = context.scopes.symbolFor(bindingIdentifier);
9171
+ if (!symbol) return false;
9172
+ return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
9173
+ };
9174
+ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
9175
+ if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
9176
+ const releaseFunction = findEnclosingFunction(releaseNode);
9177
+ if (!releaseFunction) return true;
9178
+ if (releaseFunction === findEnclosingFunction(usage.node)) return true;
9179
+ return isPotentiallyReachableFunction(releaseFunction, context);
8848
9180
  };
8849
- const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8850
- const collectFileReleaseVerbNames = (anyNode) => {
8851
- let programNode = anyNode;
9181
+ const fileContainsReleaseForUsage = (usage, context) => {
9182
+ let programNode = usage.node;
8852
9183
  while (programNode.parent) programNode = programNode.parent;
8853
- const cached = fileReleaseVerbNamesCache.get(programNode);
8854
- if (cached) return cached;
8855
- const releaseVerbNames = /* @__PURE__ */ new Set();
9184
+ let didFindRelease = false;
8856
9185
  walkAst(programNode, (child) => {
8857
- const releaseVerbName = getReleaseVerbName(child);
8858
- if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
9186
+ if (didFindRelease) return false;
9187
+ if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
9188
+ didFindRelease = true;
9189
+ return false;
9190
+ }
8859
9191
  });
8860
- fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8861
- return releaseVerbNames;
9192
+ return didFindRelease;
9193
+ };
9194
+ const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
9195
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
9196
+ if (!bindingIdentifier) return false;
9197
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
9198
+ const isSubscribeBinding = (candidateBinding) => {
9199
+ const symbol = context.scopes.symbolFor(candidateBinding);
9200
+ if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
9201
+ visitedSymbolIds.add(symbol.id);
9202
+ return symbol.references.every((reference) => {
9203
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
9204
+ const referenceParent = referenceRoot.parent;
9205
+ if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
9206
+ const aliasDeclaration = referenceParent?.parent;
9207
+ return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
9208
+ });
9209
+ };
9210
+ return isSubscribeBinding(bindingIdentifier);
8862
9211
  };
8863
- const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8864
- const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8865
- const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8866
- if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8867
- for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
9212
+ const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
9213
+ let currentNode = resourceNode;
9214
+ let parentNode = currentNode.parent;
9215
+ while (parentNode) {
9216
+ if (isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode) return true;
9217
+ if (isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode && allowConciseReturnEscape) return true;
9218
+ if (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
9219
+ currentNode = parentNode;
9220
+ parentNode = currentNode.parent;
9221
+ continue;
9222
+ }
9223
+ return false;
9224
+ }
8868
9225
  return false;
8869
9226
  };
8870
- const findRetainedFunctionLeak = (retainedFunction) => {
9227
+ const findRetainedFunctionLeak = (retainedFunction, context) => {
8871
9228
  if (!isFunctionLike$1(retainedFunction)) return null;
8872
9229
  const body = retainedFunction.body;
8873
9230
  if (!body) return null;
8874
- const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8875
9231
  let leak = null;
9232
+ const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
9233
+ const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
9234
+ const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
8876
9235
  walkAst(body, (child) => {
8877
9236
  if (leak !== null) return false;
8878
9237
  if (isFunctionLike$1(child)) return false;
8879
- if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8880
- if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8881
- leak = {
9238
+ if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
9239
+ const socketUsage = {
8882
9240
  kind: "socket",
8883
9241
  node: child,
8884
- resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
9242
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket",
9243
+ handleKey: findAssignedResourceKey(child, context),
9244
+ receiverKey: null,
9245
+ registrationVerbName: null,
9246
+ eventKey: null,
9247
+ handlerKey: null
8885
9248
  };
8886
- return false;
9249
+ if (!hasReleaseForUsage(socketUsage)) {
9250
+ leak = socketUsage;
9251
+ return false;
9252
+ }
8887
9253
  }
8888
9254
  if (!isNodeOfType(child, "CallExpression")) return;
8889
- if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8890
- leak = {
9255
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
9256
+ const timerUsage = {
8891
9257
  kind: "timer",
8892
9258
  node: child,
8893
- resourceName: "setInterval"
9259
+ resourceName: "setInterval",
9260
+ handleKey: findAssignedResourceKey(child, context),
9261
+ receiverKey: null,
9262
+ registrationVerbName: "setInterval",
9263
+ eventKey: null,
9264
+ handlerKey: null
8894
9265
  };
8895
- return false;
9266
+ if (!hasReleaseForUsage(timerUsage)) {
9267
+ leak = timerUsage;
9268
+ return false;
9269
+ }
8896
9270
  }
8897
- if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8898
- const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8899
- if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
9271
+ if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
9272
+ const registrationDetails = getCallRegistrationDetails(child, context);
9273
+ const subscriptionUsage = {
8900
9274
  kind: "subscribe",
8901
9275
  node: child,
8902
- resourceName: registrationVerbName
9276
+ resourceName: registrationDetails.registrationVerbName ?? "subscribe",
9277
+ handleKey: findAssignedResourceKey(child, context),
9278
+ ...registrationDetails
8903
9279
  };
9280
+ if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
8904
9281
  return false;
8905
9282
  }
8906
9283
  });
@@ -8912,6 +9289,26 @@ const isRetainedComponentScopeFunction = (functionNode) => {
8912
9289
  if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8913
9290
  return enclosingComponentOrHookName(functionNode) !== null;
8914
9291
  };
9292
+ const isDirectJsxEventHandlerValue = (expression) => {
9293
+ const expressionRoot = findTransparentExpressionRoot(expression);
9294
+ const expressionContainer = expressionRoot.parent;
9295
+ return isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === expressionRoot && isEventHandlerAttribute(expressionContainer.parent);
9296
+ };
9297
+ const isInlineRetainedHandlerFunction = (functionNode, context) => {
9298
+ if (!isFunctionLike$1(functionNode)) return false;
9299
+ const functionRoot = findTransparentExpressionRoot(functionNode);
9300
+ const callbackCall = functionRoot.parent;
9301
+ if (isNodeOfType(callbackCall, "CallExpression") && callbackCall.arguments?.[0] === functionRoot && isHookCall$2(callbackCall, "useCallback") && isDirectJsxEventHandlerValue(callbackCall)) return true;
9302
+ const parentNode = functionNode.parent;
9303
+ if (isDirectJsxEventHandlerValue(functionNode)) return true;
9304
+ if (!isNodeOfType(parentNode, "Property") || parentNode.value !== functionNode || parentNode.computed) return false;
9305
+ const propertyName = getStaticPropertyKeyName(parentNode);
9306
+ if (!propertyName || !/^on[A-Z]/.test(propertyName)) return false;
9307
+ const objectExpression = parentNode.parent;
9308
+ if (!isNodeOfType(objectExpression, "ObjectExpression")) return false;
9309
+ const objectParent = objectExpression.parent;
9310
+ return (isNodeOfType(objectParent, "CallExpression") && objectParent.arguments.some((argument) => argument === objectExpression) || isNodeOfType(objectParent, "JSXExpressionContainer")) && findRenderPhaseComponentOrHook(parentNode, context.scopes) !== null;
9311
+ };
8915
9312
  const effectNeedsCleanup = defineRule({
8916
9313
  id: "effect-needs-cleanup",
8917
9314
  title: "Effect subscription or timer never cleaned up",
@@ -8920,7 +9317,8 @@ const effectNeedsCleanup = defineRule({
8920
9317
  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.",
8921
9318
  create: (context) => {
8922
9319
  const reportRetainedLeak = (retainedFunction) => {
8923
- const leak = findRetainedFunctionLeak(retainedFunction);
9320
+ if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
9321
+ const leak = findRetainedFunctionLeak(retainedFunction, context);
8924
9322
  if (!leak) return;
8925
9323
  const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8926
9324
  context.report({
@@ -8932,30 +9330,31 @@ const effectNeedsCleanup = defineRule({
8932
9330
  CallExpression(node) {
8933
9331
  if (isHookCall$2(node, "useCallback")) {
8934
9332
  const retainedCallback = getEffectCallback(node);
8935
- if (retainedCallback) reportRetainedLeak(retainedCallback);
9333
+ if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
8936
9334
  return;
8937
9335
  }
8938
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
9336
+ if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
8939
9337
  const callback = getEffectCallback(node);
8940
9338
  if (!callback) return;
8941
- const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
9339
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
8942
9340
  if (usages.length === 0) return;
8943
- if (effectHasCleanupReturn(callback, usages)) return;
8944
- const firstUsage = usages[0];
9341
+ const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
9342
+ if (!firstUsage) return;
8945
9343
  const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
9344
+ const hookName = getCalleeName$2(node) ?? "effect";
8946
9345
  context.report({
8947
9346
  node,
8948
- message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
9347
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8949
9348
  });
8950
9349
  },
8951
9350
  FunctionDeclaration(node) {
8952
9351
  if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8953
9352
  },
8954
9353
  ArrowFunctionExpression(node) {
8955
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9354
+ if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
8956
9355
  },
8957
9356
  FunctionExpression(node) {
8958
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9357
+ if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
8959
9358
  }
8960
9359
  };
8961
9360
  }
@@ -9569,13 +9968,18 @@ const unwrapExpression$3 = (node) => {
9569
9968
  return current;
9570
9969
  };
9571
9970
  /**
9572
- * Get the hook name from a call expression's callee, regardless of
9573
- * whether the hook is called as `useFoo()` (Identifier) or
9574
- * `React.useFoo()` (MemberExpression).
9971
+ * Get the hook name from a direct, wrapped, namespaced, or immutable
9972
+ * React import alias call.
9575
9973
  */
9576
- const getHookName = (callee) => {
9577
- if (isNodeOfType(callee, "Identifier")) return callee.name;
9578
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9974
+ const getHookName = (callee, scopes) => {
9975
+ const strippedCallee = unwrapExpression$3(callee);
9976
+ if (isNodeOfType(strippedCallee, "Identifier")) {
9977
+ const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
9978
+ const importDeclaration = resolvedSymbol?.declarationNode.parent;
9979
+ if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
9980
+ return strippedCallee.name;
9981
+ }
9982
+ if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
9579
9983
  return null;
9580
9984
  };
9581
9985
  const FUNCTION_SCOPE_KINDS = new Set([
@@ -9760,7 +10164,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
9760
10164
  * - primitive-literal local consts (the value never changes
9761
10165
  * between renders unless the literal does)
9762
10166
  */
9763
- const symbolHasStableHookOrigin = (symbol) => {
10167
+ const symbolHasStableHookOrigin = (symbol, scopes) => {
9764
10168
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
9765
10169
  let declarator = symbol.declarationNode;
9766
10170
  while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
@@ -9773,7 +10177,7 @@ const symbolHasStableHookOrigin = (symbol) => {
9773
10177
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
9774
10178
  }
9775
10179
  if (!isNodeOfType(initializer, "CallExpression")) return false;
9776
- const initializerHookName = getHookName(initializer.callee);
10180
+ const initializerHookName = getHookName(initializer.callee, scopes);
9777
10181
  if (!initializerHookName) return false;
9778
10182
  if (initializerHookName === "useRef") return true;
9779
10183
  if (initializerHookName === "useEffectEvent") return true;
@@ -9808,7 +10212,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
9808
10212
  if (!declarator.init) return false;
9809
10213
  const initializer = unwrapExpression$3(declarator.init);
9810
10214
  if (!isNodeOfType(initializer, "CallExpression")) return false;
9811
- const initializerHookName = getHookName(initializer.callee);
10215
+ const initializerHookName = getHookName(initializer.callee, scopes);
9812
10216
  if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
9813
10217
  const depsArgument = initializer.arguments[1];
9814
10218
  if (!depsArgument || !isAstNode(depsArgument)) return false;
@@ -9838,7 +10242,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
9838
10242
  }
9839
10243
  return null;
9840
10244
  };
9841
- const isStableRefContainerCapture = (symbol, depKey) => {
10245
+ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
9842
10246
  if (symbol.kind !== "const") return false;
9843
10247
  if (!depKey.startsWith(`${symbol.name}.`)) return false;
9844
10248
  if (symbol.references.some((reference) => reference.flag !== "read")) return false;
@@ -9852,7 +10256,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
9852
10256
  if (!propertyValue) return false;
9853
10257
  currentValue = propertyValue;
9854
10258
  }
9855
- return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
10259
+ return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
9856
10260
  };
9857
10261
  const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9858
10262
  if (visitedSymbolIds.has(symbol.id)) return true;
@@ -9870,7 +10274,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9870
10274
  }
9871
10275
  return true;
9872
10276
  };
9873
- const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
10277
+ const symbolHasStableImportedAlias = (symbol, scopes) => {
10278
+ if (symbol.kind !== "const") return false;
10279
+ if (symbol.references.some((reference) => reference.flag !== "read")) return false;
10280
+ const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
10281
+ return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
10282
+ };
10283
+ const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
9874
10284
  //#endregion
9875
10285
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9876
10286
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
@@ -10058,10 +10468,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
10058
10468
  }
10059
10469
  const depKey = computeDepKey(reference);
10060
10470
  if (!depKey) continue;
10061
- if (isStableRefContainerCapture(symbol, depKey)) {
10471
+ if (isStableRefContainerCapture(symbol, depKey, scopes)) {
10062
10472
  stableCapturedNames.add(depKey);
10063
10473
  continue;
10064
10474
  }
10475
+ if (depKey === symbol.name) {
10476
+ const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
10477
+ if (identitySourceKeys) {
10478
+ if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
10479
+ for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
10480
+ continue;
10481
+ }
10482
+ }
10065
10483
  keys.add(depKey);
10066
10484
  }
10067
10485
  return {
@@ -10090,12 +10508,60 @@ const hasComputedMemberExpression = (node) => {
10090
10508
  if (stripped.computed) return true;
10091
10509
  return hasComputedMemberExpression(stripped.object);
10092
10510
  };
10511
+ const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
10512
+ const identitySourceKeys = /* @__PURE__ */ new Set();
10513
+ for (const expression of expressions) {
10514
+ const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
10515
+ if (!expressionSourceKeys) return null;
10516
+ for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
10517
+ }
10518
+ return identitySourceKeys;
10519
+ };
10520
+ const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
10521
+ const stripped = unwrapExpression$3(expression);
10522
+ 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();
10523
+ if (isNodeOfType(stripped, "Identifier")) {
10524
+ const sourceSymbol = scopes.symbolFor(stripped);
10525
+ if (!sourceSymbol) return null;
10526
+ if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
10527
+ if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
10528
+ if (visitedSymbolIds.has(sourceSymbol.id)) return null;
10529
+ visitedSymbolIds.add(sourceSymbol.id);
10530
+ const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
10531
+ visitedSymbolIds.delete(sourceSymbol.id);
10532
+ if (sourceKeys) return sourceKeys;
10533
+ }
10534
+ return new Set([sourceSymbol.name]);
10535
+ }
10536
+ if (isNodeOfType(stripped, "MemberExpression")) {
10537
+ if (hasComputedMemberExpression(stripped)) return null;
10538
+ const sourceKey = stringifyMemberChain(stripped);
10539
+ const rootIdentifier = getMemberRootIdentifier(stripped);
10540
+ const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
10541
+ if (!sourceKey || !rootSymbol) return null;
10542
+ if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
10543
+ if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
10544
+ if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
10545
+ return new Set([sourceKey]);
10546
+ }
10547
+ if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
10548
+ if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
10549
+ stripped.test,
10550
+ stripped.consequent,
10551
+ stripped.alternate
10552
+ ], scopes, visitedSymbolIds);
10553
+ return null;
10554
+ };
10555
+ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
10556
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
10557
+ return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
10558
+ };
10093
10559
  const isUseCallbackResultDep = (node, scopes) => {
10094
10560
  const rootSymbol = getRootSymbol(node, scopes);
10095
10561
  const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
10096
- return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
10562
+ return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
10097
10563
  };
10098
- const isExtraEffectDepAllowed = (node, scopes) => {
10564
+ const isExtraReactiveDepAllowed = (node, scopes) => {
10099
10565
  const rootIdentifier = getMemberRootIdentifier(node);
10100
10566
  if (!rootIdentifier) return false;
10101
10567
  const symbol = scopes.symbolFor(rootIdentifier);
@@ -10123,10 +10589,17 @@ const isUnstableInitializer = (node) => {
10123
10589
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
10124
10590
  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");
10125
10591
  };
10592
+ const isExtraDepAllowedForHook = (hookName, node, scopes) => {
10593
+ if (!isExtraReactiveDepAllowed(node, scopes)) return false;
10594
+ if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
10595
+ if (hookName !== "useMemo") return false;
10596
+ const rootSymbol = getRootSymbol(node, scopes);
10597
+ return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
10598
+ };
10126
10599
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
10127
10600
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
10128
- const isStableSetterLikeSymbol = (symbol) => {
10129
- if (!symbolHasStableHookOrigin(symbol)) return false;
10601
+ const isStableSetterLikeSymbol = (symbol, scopes) => {
10602
+ if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
10130
10603
  return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
10131
10604
  };
10132
10605
  const findStableSetterReference = (node, scopes) => {
@@ -10136,7 +10609,7 @@ const findStableSetterReference = (node, scopes) => {
10136
10609
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
10137
10610
  if (isNodeOfType(current, "Identifier")) {
10138
10611
  const symbol = scopes.referenceFor(current)?.resolvedSymbol;
10139
- if (symbol && isStableSetterLikeSymbol(symbol)) {
10612
+ if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
10140
10613
  setterName = symbol.name;
10141
10614
  return;
10142
10615
  }
@@ -10252,11 +10725,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
10252
10725
  }
10253
10726
  return false;
10254
10727
  };
10255
- const isSeededDataRefSymbol = (refSymbol) => {
10728
+ const isSeededDataRefSymbol = (refSymbol, scopes) => {
10256
10729
  if (!refSymbol) return false;
10257
10730
  const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
10258
10731
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
10259
- if (getHookName(initializer.callee) !== "useRef") return false;
10732
+ if (getHookName(initializer.callee, scopes) !== "useRef") return false;
10260
10733
  const firstArgument = initializer.arguments[0];
10261
10734
  if (!firstArgument || !isAstNode(firstArgument)) return false;
10262
10735
  const strippedArgument = unwrapExpression$3(firstArgument);
@@ -10296,7 +10769,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
10296
10769
  const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
10297
10770
  return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
10298
10771
  };
10299
- const hasMemberCallForRoot = (node, rootName) => {
10772
+ const hasMemberCallForRoot = (node, rootName, scopes) => {
10773
+ const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
10774
+ if (!rootSymbol) return false;
10300
10775
  let didFindMemberCall = false;
10301
10776
  const visit = (current) => {
10302
10777
  if (didFindMemberCall) return;
@@ -10312,7 +10787,7 @@ const hasMemberCallForRoot = (node, rootName) => {
10312
10787
  }
10313
10788
  chainObject = unwrapExpression$3(chainObject.object);
10314
10789
  }
10315
- if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
10790
+ if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
10316
10791
  didFindMemberCall = true;
10317
10792
  return;
10318
10793
  }
@@ -10330,9 +10805,11 @@ const hasMemberCallForRoot = (node, rootName) => {
10330
10805
  visit(node);
10331
10806
  return didFindMemberCall;
10332
10807
  };
10333
- const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
10334
- if ([...captureKeys].filter((captureKey) => captureKey.startsWith("props.")).length < 2 || declaredKeys.has("props")) return;
10335
- if (hasMemberCallForRoot(callback, "props")) captureKeys.add("props");
10808
+ const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
10809
+ const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
10810
+ if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
10811
+ if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
10812
+ if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
10336
10813
  };
10337
10814
  const exhaustiveDeps = defineRule({
10338
10815
  id: "exhaustive-deps",
@@ -10387,7 +10864,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10387
10864
  };
10388
10865
  const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
10389
10866
  return { CallExpression(node) {
10390
- const hookName = getHookName(node.callee);
10867
+ const hookName = getHookName(node.callee, context.scopes);
10391
10868
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
10392
10869
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10393
10870
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
@@ -10444,7 +10921,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10444
10921
  if (outerAssignments.length > 0) return;
10445
10922
  const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
10446
10923
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
10447
- if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
10924
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
10448
10925
  node: callbackToAnalyze,
10449
10926
  message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
10450
10927
  });
@@ -10543,7 +11020,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10543
11020
  const fullChain = stringifyMemberChain(stripped);
10544
11021
  if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
10545
11022
  const refSymbol = context.scopes.symbolFor(stripped.object);
10546
- if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
11023
+ if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
10547
11024
  if (!didReportRefCurrentDep) {
10548
11025
  context.report({
10549
11026
  node: elementNode,
@@ -10581,7 +11058,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10581
11058
  declaredKeys.add(key);
10582
11059
  declaredKeyToReportNode.set(key, elementNode);
10583
11060
  }
10584
- addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
11061
+ addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
10585
11062
  const missingCaptureKeys = [];
10586
11063
  for (const captureKey of captureKeys) {
10587
11064
  let isCoveredByDeclared = false;
@@ -10667,7 +11144,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
10667
11144
  if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
10668
11145
  if (outerFunctionCapturedNames.has(rootName)) continue;
10669
11146
  const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
10670
- if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
11147
+ if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
10671
11148
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
10672
11149
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
10673
11150
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
@@ -12339,20 +12816,6 @@ const collectHandlerReferencedNames = (root) => {
12339
12816
  return names;
12340
12817
  };
12341
12818
  //#endregion
12342
- //#region src/plugin/utils/get-function-binding-name.ts
12343
- const getFunctionBindingIdentifier = (functionNode) => {
12344
- if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
12345
- const parent = functionNode.parent;
12346
- if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
12347
- if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
12348
- if (isNodeOfType(parent, "CallExpression")) {
12349
- const callParent = parent.parent;
12350
- if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
12351
- }
12352
- return null;
12353
- };
12354
- const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
12355
- //#endregion
12356
12819
  //#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
12357
12820
  const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
12358
12821
  const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -13497,7 +13960,7 @@ const jsHoistIntl = defineRule({
13497
13960
  });
13498
13961
  //#endregion
13499
13962
  //#region src/plugin/utils/create-loop-aware-visitors.ts
13500
- const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
13963
+ const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
13501
13964
  "map",
13502
13965
  "flatMap",
13503
13966
  "forEach",
@@ -13516,7 +13979,7 @@ const isIteratorCallback = (node) => {
13516
13979
  const parent = node.parent;
13517
13980
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
13518
13981
  if (!parent.arguments.includes(node)) return false;
13519
- return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
13982
+ return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
13520
13983
  };
13521
13984
  const createLoopAwareVisitors = (innerVisitors, options = {}) => {
13522
13985
  let loopDepth = 0;
@@ -13745,7 +14208,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
13745
14208
  });
13746
14209
  return indexedArrayObject;
13747
14210
  };
13748
- const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
14211
+ const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
13749
14212
  const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
13750
14213
  const LENGTH_MISMATCH_OPERATORS = new Set([
13751
14214
  "!==",
@@ -13757,17 +14220,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
13757
14220
  ]);
13758
14221
  const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
13759
14222
  const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
13760
- const binaryGuard = unwrapChainExpression$2(candidate);
14223
+ const binaryGuard = unwrapChainExpression$3(candidate);
13761
14224
  if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
13762
14225
  if (!operators.has(binaryGuard.operator)) return false;
13763
- const leftSide = unwrapChainExpression$2(binaryGuard.left);
13764
- const rightSide = unwrapChainExpression$2(binaryGuard.right);
14226
+ const leftSide = unwrapChainExpression$3(binaryGuard.left);
14227
+ const rightSide = unwrapChainExpression$3(binaryGuard.right);
13765
14228
  if (!isMemberProperty(leftSide, "length")) return false;
13766
14229
  if (!isMemberProperty(rightSide, "length")) return false;
13767
- const leftLengthObject = unwrapChainExpression$2(leftSide.object);
13768
- const rightLengthObject = unwrapChainExpression$2(rightSide.object);
13769
- const normalizedReceiver = unwrapChainExpression$2(receiverArray);
13770
- const normalizedIndexed = unwrapChainExpression$2(indexedArray);
14230
+ const leftLengthObject = unwrapChainExpression$3(leftSide.object);
14231
+ const rightLengthObject = unwrapChainExpression$3(rightSide.object);
14232
+ const normalizedReceiver = unwrapChainExpression$3(receiverArray);
14233
+ const normalizedIndexed = unwrapChainExpression$3(indexedArray);
13771
14234
  const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
13772
14235
  const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
13773
14236
  return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
@@ -13854,18 +14317,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
13854
14317
  "toReversed"
13855
14318
  ]);
13856
14319
  const peelLengthPreservingDerivation = (expression) => {
13857
- let current = unwrapChainExpression$2(expression);
14320
+ let current = unwrapChainExpression$3(expression);
13858
14321
  for (;;) {
13859
14322
  if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
13860
- current = unwrapChainExpression$2(current.elements[0].argument);
14323
+ current = unwrapChainExpression$3(current.elements[0].argument);
13861
14324
  continue;
13862
14325
  }
13863
14326
  if (isNodeOfType(current, "CallExpression")) {
13864
- const callee = unwrapChainExpression$2(current.callee);
14327
+ const callee = unwrapChainExpression$3(current.callee);
13865
14328
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
13866
- const calleeObject = unwrapChainExpression$2(callee.object);
14329
+ const calleeObject = unwrapChainExpression$3(callee.object);
13867
14330
  if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
13868
- current = unwrapChainExpression$2(current.arguments[0]);
14331
+ current = unwrapChainExpression$3(current.arguments[0]);
13869
14332
  continue;
13870
14333
  }
13871
14334
  if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
@@ -13910,7 +14373,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
13910
14373
  const initializer = findConstInitializer(current.name, scopeNode);
13911
14374
  if (!initializer) return current;
13912
14375
  const peeledInitializer = peelLengthPreservingDerivation(initializer);
13913
- if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
14376
+ if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
13914
14377
  current = peeledInitializer;
13915
14378
  }
13916
14379
  return current;
@@ -19640,54 +20103,6 @@ const nextjsNoAElement = defineRule({
19640
20103
  } })
19641
20104
  });
19642
20105
  //#endregion
19643
- //#region src/plugin/utils/collect-effect-invoked-functions.ts
19644
- const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
19645
- "then",
19646
- "catch",
19647
- "finally"
19648
- ]);
19649
- const collectEffectInvokedFunctions = (effectCallback) => {
19650
- const invokedFunctions = new Set([effectCallback]);
19651
- const localFunctionBindings = /* @__PURE__ */ new Map();
19652
- const calledBindingNames = /* @__PURE__ */ new Set();
19653
- const pendingFunctions = [effectCallback];
19654
- const enqueue = (candidate) => {
19655
- const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
19656
- if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
19657
- invokedFunctions.add(strippedCandidate);
19658
- pendingFunctions.push(strippedCandidate);
19659
- };
19660
- const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
19661
- while (pendingFunctions.length > 0) {
19662
- const currentFunction = pendingFunctions.pop();
19663
- if (!currentFunction) break;
19664
- walkAst(currentFunction, (child) => {
19665
- if (child !== currentFunction && isFunctionLike$1(child)) {
19666
- if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
19667
- return false;
19668
- }
19669
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
19670
- const initializer = child.init ? stripParenExpression(child.init) : null;
19671
- if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
19672
- return;
19673
- }
19674
- if (!isNodeOfType(child, "CallExpression")) return;
19675
- const callee = stripParenExpression(child.callee);
19676
- if (isFunctionLike$1(callee)) {
19677
- enqueue(callee);
19678
- return;
19679
- }
19680
- if (isNodeOfType(callee, "Identifier")) {
19681
- calledBindingNames.add(callee.name);
19682
- return;
19683
- }
19684
- if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
19685
- });
19686
- for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
19687
- }
19688
- return invokedFunctions;
19689
- };
19690
- //#endregion
19691
20106
  //#region src/plugin/utils/contains-fetch-call.ts
19692
20107
  const isFetchCall$1 = (node) => {
19693
20108
  if (!isNodeOfType(node, "CallExpression")) return false;
@@ -20671,6 +21086,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20671
21086
  continue;
20672
21087
  }
20673
21088
  if (isNodeOfType(statement, "ExportNamedDeclaration")) {
21089
+ if (statement.exportKind === "type") continue;
20674
21090
  const declaration = statement.declaration;
20675
21091
  if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
20676
21092
  recordVariableDeclaration(declaration);
@@ -20685,6 +21101,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20685
21101
  }
20686
21102
  for (const specifier of statement.specifiers ?? []) {
20687
21103
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21104
+ if (specifier.exportKind === "type") continue;
20688
21105
  const local = specifier.local;
20689
21106
  const exported = specifier.exported;
20690
21107
  if (!isNodeOfType(local, "Identifier")) continue;
@@ -20733,25 +21150,37 @@ const resolveImportedExportName = (importSpecifier) => {
20733
21150
  if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
20734
21151
  return null;
20735
21152
  };
20736
- const findReExportSourcesForName = (programRoot, exportedName) => {
21153
+ const findReExportTargetsForName = (programRoot, exportedName) => {
20737
21154
  if (!isNodeOfType(programRoot, "Program")) return [];
20738
- const exportAllSources = [];
21155
+ const exportAllTargets = [];
20739
21156
  for (const statement of programRoot.body ?? []) {
20740
21157
  if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
21158
+ if (statement.exportKind === "type") continue;
20741
21159
  const sourceValue = statement.source.value;
20742
21160
  if (typeof sourceValue !== "string") continue;
20743
21161
  for (const specifier of statement.specifiers ?? []) {
20744
21162
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21163
+ if (specifier.exportKind === "type") continue;
20745
21164
  const exported = specifier.exported;
20746
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
21165
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
21166
+ const local = specifier.local;
21167
+ const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
21168
+ if (importedName) return [{
21169
+ importedName,
21170
+ source: sourceValue
21171
+ }];
20747
21172
  }
20748
21173
  }
20749
21174
  if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
21175
+ if (statement.exportKind === "type" || statement.exported) continue;
20750
21176
  const sourceValue = statement.source.value;
20751
- if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
21177
+ if (typeof sourceValue === "string") exportAllTargets.push({
21178
+ importedName: exportedName,
21179
+ source: sourceValue
21180
+ });
20752
21181
  }
20753
21182
  }
20754
- return exportAllSources;
21183
+ return exportAllTargets;
20755
21184
  };
20756
21185
  //#endregion
20757
21186
  //#region src/plugin/utils/attach-parent-references.ts
@@ -20840,139 +21269,6 @@ const parseSourceFile = (absoluteFilePath) => {
20840
21269
  return parsedProgram;
20841
21270
  };
20842
21271
  //#endregion
20843
- //#region src/plugin/utils/is-barrel-index-module.ts
20844
- const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
20845
- const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
20846
- const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
20847
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
20848
- const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
20849
- const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
20850
- const createNonBarrelInfo = () => ({
20851
- isBarrel: false,
20852
- exportsByName: /* @__PURE__ */ new Map(),
20853
- starExportSources: []
20854
- });
20855
- const addImportedBinding = (importedBindings, binding) => {
20856
- importedBindings.set(binding.localName, {
20857
- ...binding,
20858
- didExport: false
20859
- });
20860
- };
20861
- const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
20862
- for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
20863
- localName: specifier.exportedName,
20864
- importedName: specifier.localName,
20865
- source,
20866
- isTypeOnly: specifier.isTypeOnly
20867
- });
20868
- };
20869
- const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
20870
- const trimmedImportClause = importClause.trim();
20871
- const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
20872
- if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
20873
- localName: namespaceMatch[1],
20874
- importedName: "*",
20875
- source,
20876
- isTypeOnly: declarationIsTypeOnly
20877
- });
20878
- const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
20879
- if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
20880
- const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
20881
- if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
20882
- localName: defaultImportName,
20883
- importedName: "default",
20884
- source,
20885
- isTypeOnly: declarationIsTypeOnly
20886
- });
20887
- };
20888
- const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
20889
- let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
20890
- collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
20891
- return "";
20892
- });
20893
- withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
20894
- const isTypeOnly = Boolean(typeKeyword);
20895
- if (namespaceExportName) {
20896
- exportsByName.set(namespaceExportName, {
20897
- exportedName: namespaceExportName,
20898
- importedName: "*",
20899
- source,
20900
- isTypeOnly
20901
- });
20902
- return "";
20903
- }
20904
- if (specifiersText) {
20905
- for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
20906
- exportedName: specifier.exportedName,
20907
- importedName: specifier.localName,
20908
- source,
20909
- isTypeOnly: specifier.isTypeOnly
20910
- });
20911
- return "";
20912
- }
20913
- starExportSources.push(source);
20914
- return "";
20915
- });
20916
- withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
20917
- for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
20918
- const importedBinding = importedBindings.get(specifier.localName);
20919
- if (!importedBinding) return _match;
20920
- importedBinding.didExport = true;
20921
- exportsByName.set(specifier.exportedName, {
20922
- exportedName: specifier.exportedName,
20923
- importedName: importedBinding.importedName,
20924
- source: importedBinding.source,
20925
- isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
20926
- });
20927
- }
20928
- return "";
20929
- });
20930
- return withoutKnownDeclarations;
20931
- };
20932
- const hasUnexportedRuntimeImport = (importedBindings) => {
20933
- for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
20934
- return false;
20935
- };
20936
- const classifyBarrelModule = (sourceText) => {
20937
- const strippedSource = stripJsComments(sourceText).trim();
20938
- if (!strippedSource) return createNonBarrelInfo();
20939
- const importedBindings = /* @__PURE__ */ new Map();
20940
- const exportsByName = /* @__PURE__ */ new Map();
20941
- const starExportSources = [];
20942
- if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
20943
- return {
20944
- isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
20945
- exportsByName,
20946
- starExportSources
20947
- };
20948
- };
20949
- const getBarrelIndexModuleInfo = (filePath) => {
20950
- if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
20951
- recordContentProbe(filePath);
20952
- let fileStat;
20953
- try {
20954
- fileStat = fs.statSync(filePath);
20955
- } catch {
20956
- fileStat = null;
20957
- }
20958
- const cachedResult = barrelIndexModuleInfoCache.get(filePath);
20959
- if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
20960
- if (fileStat === null) return createNonBarrelInfo();
20961
- let moduleInfo = createNonBarrelInfo();
20962
- try {
20963
- moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
20964
- } catch {
20965
- moduleInfo = createNonBarrelInfo();
20966
- }
20967
- barrelIndexModuleInfoCache.set(filePath, {
20968
- mtimeMs: fileStat.mtimeMs,
20969
- size: fileStat.size,
20970
- moduleInfo
20971
- });
20972
- return moduleInfo;
20973
- };
20974
- const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
20975
- //#endregion
20976
21272
  //#region src/plugin/utils/resolve-relative-import-path.ts
20977
21273
  const MODULE_FILE_EXTENSIONS = [
20978
21274
  ".ts",
@@ -21089,35 +21385,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
21089
21385
  };
21090
21386
  const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
21091
21387
  //#endregion
21092
- //#region src/plugin/utils/resolve-barrel-export-file-path.ts
21093
- const getUniqueFilePath = (filePaths) => {
21094
- const uniqueFilePaths = new Set(filePaths);
21095
- if (uniqueFilePaths.size !== 1) return null;
21096
- const [filePath] = uniqueFilePaths;
21097
- return filePath ?? null;
21098
- };
21099
- const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
21100
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
21101
- if (!resolvedTargetPath) return null;
21102
- const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
21103
- if (nestedTargetPath) return nestedTargetPath;
21104
- return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
21105
- };
21106
- const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
21107
- if (visitedFilePaths.has(barrelFilePath)) return null;
21108
- visitedFilePaths.add(barrelFilePath);
21109
- const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
21110
- if (!moduleInfo.isBarrel) return null;
21111
- const target = moduleInfo.exportsByName.get(exportedName);
21112
- if (target) {
21113
- const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
21114
- if (!resolvedTargetPath) return null;
21115
- return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
21116
- }
21117
- if (exportedName === "default") return null;
21118
- return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
21119
- };
21120
- //#endregion
21121
21388
  //#region src/plugin/utils/resolve-tsconfig-alias.ts
21122
21389
  const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
21123
21390
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
@@ -21296,25 +21563,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
21296
21563
  };
21297
21564
  //#endregion
21298
21565
  //#region src/plugin/utils/resolve-module-path.ts
21299
- const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
21566
+ const resolveModulePath = (fromFilename, source) => {
21567
+ if (path.isAbsolute(source)) return null;
21568
+ return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
21569
+ };
21300
21570
  //#endregion
21301
21571
  //#region src/plugin/utils/resolve-cross-file-function-export.ts
21302
21572
  const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
21303
21573
  if (visitedFilePaths.size >= 4) return null;
21304
21574
  if (visitedFilePaths.has(filePath)) return null;
21305
21575
  visitedFilePaths.add(filePath);
21306
- const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
21307
- const programRoot = parseSourceFile(actualFilePath);
21576
+ const programRoot = parseSourceFile(filePath);
21308
21577
  if (!programRoot) return null;
21309
21578
  const exported = findExportedFunctionBody(programRoot, exportedName);
21310
21579
  if (exported) return exported;
21311
- for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
21312
- const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
21580
+ const resolvedCandidates = /* @__PURE__ */ new Set();
21581
+ for (const target of findReExportTargetsForName(programRoot, exportedName)) {
21582
+ const nextFilePath = resolveModulePath(filePath, target.source);
21313
21583
  if (!nextFilePath) continue;
21314
- const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
21315
- if (resolved) return resolved;
21584
+ const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
21585
+ if (resolved) resolvedCandidates.add(resolved);
21316
21586
  }
21317
- return null;
21587
+ if (resolvedCandidates.size !== 1) return null;
21588
+ return resolvedCandidates.values().next().value ?? null;
21318
21589
  };
21319
21590
  const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
21320
21591
  const resolvedFilePath = resolveModulePath(fromFilename, source);
@@ -22099,6 +22370,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22099
22370
  const importDeclaration = declarationNode.parent;
22100
22371
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22101
22372
  }));
22373
+ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
22374
+ if (def.type !== "ImportBinding") return false;
22375
+ const declarationNode = def.node;
22376
+ if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
22377
+ const importDeclaration = declarationNode.parent;
22378
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22379
+ }));
22380
+ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
22381
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
22382
+ const callee = stripParenExpression(declarator.init.callee);
22383
+ if (isNodeOfType(callee, "Identifier")) {
22384
+ const reference = getRef(analysis, callee);
22385
+ if (!reference?.resolved) return callee.name === hookName;
22386
+ return isReactNamedImportReference(reference, hookName);
22387
+ }
22388
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
22389
+ const namespaceReference = getRef(analysis, callee.object);
22390
+ if (!namespaceReference?.resolved) return callee.object.name === "React";
22391
+ return isReactNamespaceImportReference(namespaceReference);
22392
+ };
22102
22393
  const isHookCallee$1 = (analysis, node, hookName) => {
22103
22394
  if (!node) return false;
22104
22395
  if (isNodeOfType(node, "Identifier")) {
@@ -22497,6 +22788,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22497
22788
  "parseInt"
22498
22789
  ]);
22499
22790
  const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22791
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22792
+ "isRawJSON",
22793
+ "parse",
22794
+ "rawJSON",
22795
+ "stringify"
22796
+ ])], ["Math", new Set([
22797
+ "abs",
22798
+ "acos",
22799
+ "acosh",
22800
+ "asin",
22801
+ "asinh",
22802
+ "atan",
22803
+ "atan2",
22804
+ "atanh",
22805
+ "cbrt",
22806
+ "ceil",
22807
+ "clz32",
22808
+ "cos",
22809
+ "cosh",
22810
+ "exp",
22811
+ "floor",
22812
+ "fround",
22813
+ "hypot",
22814
+ "imul",
22815
+ "log",
22816
+ "log10",
22817
+ "log1p",
22818
+ "log2",
22819
+ "max",
22820
+ "min",
22821
+ "pow",
22822
+ "round",
22823
+ "sign",
22824
+ "sin",
22825
+ "sinh",
22826
+ "sqrt",
22827
+ "tan",
22828
+ "tanh",
22829
+ "trunc"
22830
+ ])]]);
22500
22831
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22501
22832
  "concat",
22502
22833
  "filter",
@@ -22543,6 +22874,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
22543
22874
  }
22544
22875
  return parameter;
22545
22876
  };
22877
+ const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
22878
+ const isModuleFunction = (functionNode) => {
22879
+ let ancestor = functionNode.parent;
22880
+ while (ancestor) {
22881
+ if (isFunctionLike$1(ancestor)) return false;
22882
+ if (isNodeOfType(ancestor, "Program")) return true;
22883
+ ancestor = ancestor.parent;
22884
+ }
22885
+ return false;
22886
+ };
22887
+ const getFunctionBindingNames = (functionNode) => {
22888
+ const names = /* @__PURE__ */ new Set();
22889
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
22890
+ const parent = functionNode.parent;
22891
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
22892
+ return names;
22893
+ };
22894
+ const collectModuleBindingNames = (functionNode) => {
22895
+ let program = functionNode.parent;
22896
+ while (program && !isNodeOfType(program, "Program")) program = program.parent;
22897
+ const bindingNames = /* @__PURE__ */ new Set();
22898
+ if (!program || !isNodeOfType(program, "Program")) return bindingNames;
22899
+ for (const statement of program.body ?? []) {
22900
+ if (isNodeOfType(statement, "ImportDeclaration")) {
22901
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
22902
+ continue;
22903
+ }
22904
+ const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
22905
+ if (isNodeOfType(declaration, "VariableDeclaration")) {
22906
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
22907
+ continue;
22908
+ }
22909
+ if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
22910
+ }
22911
+ return bindingNames;
22912
+ };
22546
22913
  const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
22547
22914
  const substitutions = /* @__PURE__ */ new Map();
22548
22915
  const parameters = getFunctionParameters(functionNode);
@@ -22673,7 +23040,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
22673
23040
  for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
22674
23041
  return introducedBindings;
22675
23042
  };
22676
- const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
23043
+ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
22677
23044
  const effectFunction = getEffectFn(analysis, effectNode);
22678
23045
  if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
22679
23046
  const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
@@ -22682,7 +23049,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22682
23049
  invocation: null,
22683
23050
  isDeferred: false,
22684
23051
  introducedBindings: /* @__PURE__ */ new Set(),
22685
- substitutions: /* @__PURE__ */ new Map()
23052
+ substitutions: /* @__PURE__ */ new Map(),
23053
+ currentFilename
22686
23054
  };
22687
23055
  const frames = [rootFrame];
22688
23056
  walkAst(effectFunction, (child) => {
@@ -22703,7 +23071,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22703
23071
  invocation: child,
22704
23072
  isDeferred,
22705
23073
  introducedBindings,
22706
- substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
23074
+ substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
23075
+ currentFilename
22707
23076
  });
22708
23077
  };
22709
23078
  if (isFunctionLike$1(callee)) {
@@ -22756,6 +23125,174 @@ const isOpaqueHookCall = (callExpression) => {
22756
23125
  const calleeName = getCallCalleeName$1(callExpression);
22757
23126
  return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
22758
23127
  };
23128
+ const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
23129
+ let canContinue = true;
23130
+ for (const statement of statements) {
23131
+ if (!canContinue) {
23132
+ if (!isNodeOfType(statement, "EmptyStatement")) return {
23133
+ canContinue: false,
23134
+ isValid: false
23135
+ };
23136
+ continue;
23137
+ }
23138
+ if (isNodeOfType(statement, "EmptyStatement")) continue;
23139
+ if (isNodeOfType(statement, "ReturnStatement")) {
23140
+ if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
23141
+ canContinue: false,
23142
+ isValid: false
23143
+ };
23144
+ canContinue = false;
23145
+ continue;
23146
+ }
23147
+ if (isNodeOfType(statement, "BlockStatement")) {
23148
+ const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
23149
+ if (!blockSummary.isValid) return blockSummary;
23150
+ canContinue = blockSummary.canContinue;
23151
+ continue;
23152
+ }
23153
+ if (isNodeOfType(statement, "IfStatement")) {
23154
+ if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
23155
+ canContinue: false,
23156
+ isValid: false
23157
+ };
23158
+ const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
23159
+ if (!consequentSummary.isValid) return consequentSummary;
23160
+ const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
23161
+ canContinue: true,
23162
+ isValid: true
23163
+ };
23164
+ if (!alternateSummary.isValid) return alternateSummary;
23165
+ canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
23166
+ continue;
23167
+ }
23168
+ return {
23169
+ canContinue: false,
23170
+ isValid: false
23171
+ };
23172
+ }
23173
+ return {
23174
+ canContinue,
23175
+ isValid: true
23176
+ };
23177
+ };
23178
+ const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
23179
+ const node = stripParenExpression(expression);
23180
+ if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
23181
+ if (isNodeOfType(node, "Identifier")) {
23182
+ const parameterIndex = environment.parameterIndices.get(node.name);
23183
+ if (parameterIndex !== void 0) {
23184
+ if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
23185
+ return true;
23186
+ }
23187
+ return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
23188
+ }
23189
+ if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
23190
+ if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
23191
+ if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
23192
+ if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
23193
+ if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
23194
+ return analyzeHelperExpression(property.value, environment, usedParameterIndices);
23195
+ });
23196
+ if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
23197
+ if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23198
+ if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
23199
+ if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
23200
+ if (isNodeOfType(node, "MemberExpression")) {
23201
+ if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
23202
+ return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
23203
+ }
23204
+ if (isFunctionLike$1(node)) {
23205
+ if (isAsyncOrGeneratorFunction(node)) return false;
23206
+ const callbackParameterIndices = new Map(environment.parameterIndices);
23207
+ for (const parameter of getFunctionParameters(node)) {
23208
+ if (!isNodeOfType(parameter, "Identifier")) return false;
23209
+ callbackParameterIndices.set(parameter.name, null);
23210
+ }
23211
+ const callbackEnvironment = {
23212
+ parameterIndices: callbackParameterIndices,
23213
+ recursiveNames: environment.recursiveNames,
23214
+ shadowedGlobalNames: environment.shadowedGlobalNames
23215
+ };
23216
+ if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
23217
+ const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23218
+ return callbackSummary.isValid && !callbackSummary.canContinue;
23219
+ }
23220
+ if (isNodeOfType(node, "CallExpression")) {
23221
+ const callee = stripParenExpression(node.callee);
23222
+ const calleeRoot = getMemberRoot(callee);
23223
+ 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);
23224
+ const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23225
+ const namespaceMemberName = getStaticMemberName(callee);
23226
+ const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23227
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23228
+ if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23229
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23230
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23231
+ }
23232
+ if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23233
+ return false;
23234
+ };
23235
+ const helperSummaryCache = /* @__PURE__ */ new WeakMap();
23236
+ const summarizeHelperReturn = (functionNode) => {
23237
+ if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
23238
+ if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
23239
+ if (isAsyncOrGeneratorFunction(functionNode)) {
23240
+ helperSummaryCache.set(functionNode, null);
23241
+ return null;
23242
+ }
23243
+ const parameterIndices = /* @__PURE__ */ new Map();
23244
+ const parameters = getFunctionParameters(functionNode);
23245
+ for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
23246
+ const parameter = parameters[parameterIndex];
23247
+ if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
23248
+ helperSummaryCache.set(functionNode, null);
23249
+ return null;
23250
+ }
23251
+ parameterIndices.set(parameter.name, parameterIndex);
23252
+ }
23253
+ const environment = {
23254
+ parameterIndices,
23255
+ recursiveNames: getFunctionBindingNames(functionNode),
23256
+ shadowedGlobalNames: collectModuleBindingNames(functionNode)
23257
+ };
23258
+ const usedParameterIndices = /* @__PURE__ */ new Set();
23259
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) {
23260
+ if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
23261
+ helperSummaryCache.set(functionNode, null);
23262
+ return null;
23263
+ }
23264
+ } else {
23265
+ const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
23266
+ if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
23267
+ helperSummaryCache.set(functionNode, null);
23268
+ return null;
23269
+ }
23270
+ }
23271
+ const summary = { usedParameterIndices };
23272
+ helperSummaryCache.set(functionNode, summary);
23273
+ return summary;
23274
+ };
23275
+ const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
23276
+ if (!isNodeOfType(callee, "Identifier")) return null;
23277
+ const reference = getRef(analysis, callee);
23278
+ if (!reference?.resolved) return null;
23279
+ const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
23280
+ if (importDefinition) {
23281
+ if (!currentFilename) return null;
23282
+ const specifier = importDefinition.node;
23283
+ if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
23284
+ const importDeclaration = specifier.parent;
23285
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
23286
+ if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
23287
+ const source = importDeclaration.source?.value;
23288
+ if (typeof source !== "string") return null;
23289
+ const exportedName = resolveImportedExportName(specifier);
23290
+ if (!exportedName) return null;
23291
+ return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
23292
+ }
23293
+ const callable = resolveWrappedCallable(analysis, callee);
23294
+ return callable && isModuleFunction(callable) ? callable : null;
23295
+ };
22759
23296
  const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
22760
23297
  const definitionNode = definition.node;
22761
23298
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
@@ -22841,38 +23378,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
22841
23378
  const calleeRoot = getMemberRoot(callee);
22842
23379
  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;
22843
23380
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
22844
- if (isPureMemberTransform) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
22845
- for (const argument of node.arguments ?? []) {
22846
- if (isFunctionLike$1(argument)) continue;
22847
- mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
22848
- }
22849
- if (isPureGlobalCall || isPureMemberTransform) return evidence;
22850
- if (isNodeOfType(callee, "MemberExpression")) {
22851
- evidence.hasUnknownSource = true;
23381
+ if (isPureGlobalCall || isPureMemberTransform) {
23382
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23383
+ for (const argument of node.arguments ?? []) {
23384
+ if (isFunctionLike$1(argument)) continue;
23385
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23386
+ }
22852
23387
  return evidence;
22853
23388
  }
22854
23389
  if (remainingCallFrames <= 0) {
22855
23390
  evidence.hasUnknownSource = true;
22856
23391
  return evidence;
22857
23392
  }
22858
- const callable = resolveWrappedCallable(analysis, callee);
22859
- if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
22860
- evidence.hasUnknownSource = true;
23393
+ const localHelperFunction = resolveWrappedCallable(analysis, callee);
23394
+ if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
23395
+ if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
23396
+ evidence.hasUnknownSource = true;
23397
+ return evidence;
23398
+ }
23399
+ const localHelperFrame = {
23400
+ functionNode: localHelperFunction,
23401
+ invocation: node,
23402
+ isDeferred: false,
23403
+ introducedBindings: /* @__PURE__ */ new Set(),
23404
+ substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
23405
+ currentFilename: frame.currentFilename
23406
+ };
23407
+ const returnedExpressions = getReturnedExpressions(localHelperFunction);
23408
+ if (returnedExpressions.length === 0) {
23409
+ evidence.hasUnknownSource = true;
23410
+ return evidence;
23411
+ }
23412
+ for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
22861
23413
  return evidence;
22862
23414
  }
22863
- const valueFrame = {
22864
- functionNode: callable,
22865
- invocation: node,
22866
- isDeferred: false,
22867
- introducedBindings: /* @__PURE__ */ new Set(),
22868
- substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
22869
- };
22870
- const returnedExpressions = getReturnedExpressions(callable);
22871
- if (returnedExpressions.length === 0) {
23415
+ const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
23416
+ const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
23417
+ if (!helperSummary) {
22872
23418
  evidence.hasUnknownSource = true;
22873
23419
  return evidence;
22874
23420
  }
22875
- for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23421
+ const argumentsForHelper = node.arguments ?? [];
23422
+ for (const parameterIndex of helperSummary.usedParameterIndices) {
23423
+ const argument = argumentsForHelper[parameterIndex];
23424
+ if (!argument) {
23425
+ evidence.hasUnknownSource = true;
23426
+ return evidence;
23427
+ }
23428
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
23429
+ }
22876
23430
  return evidence;
22877
23431
  }
22878
23432
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
@@ -22890,6 +23444,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
22890
23444
  }
22891
23445
  return evidence;
22892
23446
  };
23447
+ const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
23448
+ const evidence = collectValueEvidence(analysis, expression, {
23449
+ functionNode: componentFunction,
23450
+ invocation: null,
23451
+ isDeferred: false,
23452
+ introducedBindings: /* @__PURE__ */ new Set(),
23453
+ substitutions: /* @__PURE__ */ new Map(),
23454
+ currentFilename
23455
+ }, 1);
23456
+ return {
23457
+ sourceReferences: evidence.sourceReferences,
23458
+ isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
23459
+ };
23460
+ };
22893
23461
  const findStateSetterReference = (analysis, callExpression) => {
22894
23462
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
22895
23463
  const callee = stripParenExpression(callExpression.callee);
@@ -22952,8 +23520,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
22952
23520
  }
22953
23521
  return false;
22954
23522
  };
22955
- const collectEffectStateWriteFacts = (analysis, effectNode) => {
22956
- const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
23523
+ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
23524
+ const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
22957
23525
  if (frames.length === 0) return [];
22958
23526
  const effectHasCleanup = hasCleanup(analysis, effectNode);
22959
23527
  const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
@@ -22973,7 +23541,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
22973
23541
  invocation: callExpression,
22974
23542
  isDeferred: frame.isDeferred,
22975
23543
  introducedBindings: collectIntroducedBindings(analysis, writtenValue),
22976
- substitutions: /* @__PURE__ */ new Map()
23544
+ substitutions: /* @__PURE__ */ new Map(),
23545
+ currentFilename
22977
23546
  };
22978
23547
  valueEvidence = emptyEvidence();
22979
23548
  const returnedExpressions = getReturnedExpressions(writtenValue);
@@ -23022,7 +23591,7 @@ const noAdjustStateOnPropChange = defineRule({
23022
23591
  const dependencyReferences = getEffectDepsRefs(analysis, node);
23023
23592
  if (!dependencyReferences) return;
23024
23593
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
23025
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
23594
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
23026
23595
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
23027
23596
  context.report({
23028
23597
  node: fact.callExpression,
@@ -24283,6 +24852,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
24283
24852
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
24284
24853
  };
24285
24854
  //#endregion
24855
+ //#region src/plugin/utils/is-barrel-index-module.ts
24856
+ const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
24857
+ const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24858
+ const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24859
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24860
+ const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
24861
+ const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
24862
+ const createNonBarrelInfo = () => ({
24863
+ isBarrel: false,
24864
+ exportsByName: /* @__PURE__ */ new Map(),
24865
+ starExportSources: []
24866
+ });
24867
+ const addImportedBinding = (importedBindings, binding) => {
24868
+ importedBindings.set(binding.localName, {
24869
+ ...binding,
24870
+ didExport: false
24871
+ });
24872
+ };
24873
+ const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
24874
+ for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
24875
+ localName: specifier.exportedName,
24876
+ importedName: specifier.localName,
24877
+ source,
24878
+ isTypeOnly: specifier.isTypeOnly
24879
+ });
24880
+ };
24881
+ const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
24882
+ const trimmedImportClause = importClause.trim();
24883
+ const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
24884
+ if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
24885
+ localName: namespaceMatch[1],
24886
+ importedName: "*",
24887
+ source,
24888
+ isTypeOnly: declarationIsTypeOnly
24889
+ });
24890
+ const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
24891
+ if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
24892
+ const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
24893
+ if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
24894
+ localName: defaultImportName,
24895
+ importedName: "default",
24896
+ source,
24897
+ isTypeOnly: declarationIsTypeOnly
24898
+ });
24899
+ };
24900
+ const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
24901
+ let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
24902
+ collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
24903
+ return "";
24904
+ });
24905
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
24906
+ const isTypeOnly = Boolean(typeKeyword);
24907
+ if (namespaceExportName) {
24908
+ exportsByName.set(namespaceExportName, {
24909
+ exportedName: namespaceExportName,
24910
+ importedName: "*",
24911
+ source,
24912
+ isTypeOnly
24913
+ });
24914
+ return "";
24915
+ }
24916
+ if (specifiersText) {
24917
+ for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
24918
+ exportedName: specifier.exportedName,
24919
+ importedName: specifier.localName,
24920
+ source,
24921
+ isTypeOnly: specifier.isTypeOnly
24922
+ });
24923
+ return "";
24924
+ }
24925
+ starExportSources.push(source);
24926
+ return "";
24927
+ });
24928
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
24929
+ for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
24930
+ const importedBinding = importedBindings.get(specifier.localName);
24931
+ if (!importedBinding) return _match;
24932
+ importedBinding.didExport = true;
24933
+ exportsByName.set(specifier.exportedName, {
24934
+ exportedName: specifier.exportedName,
24935
+ importedName: importedBinding.importedName,
24936
+ source: importedBinding.source,
24937
+ isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
24938
+ });
24939
+ }
24940
+ return "";
24941
+ });
24942
+ return withoutKnownDeclarations;
24943
+ };
24944
+ const hasUnexportedRuntimeImport = (importedBindings) => {
24945
+ for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
24946
+ return false;
24947
+ };
24948
+ const classifyBarrelModule = (sourceText) => {
24949
+ const strippedSource = stripJsComments(sourceText).trim();
24950
+ if (!strippedSource) return createNonBarrelInfo();
24951
+ const importedBindings = /* @__PURE__ */ new Map();
24952
+ const exportsByName = /* @__PURE__ */ new Map();
24953
+ const starExportSources = [];
24954
+ if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
24955
+ return {
24956
+ isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
24957
+ exportsByName,
24958
+ starExportSources
24959
+ };
24960
+ };
24961
+ const getBarrelIndexModuleInfo = (filePath) => {
24962
+ if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
24963
+ recordContentProbe(filePath);
24964
+ let fileStat;
24965
+ try {
24966
+ fileStat = fs.statSync(filePath);
24967
+ } catch {
24968
+ fileStat = null;
24969
+ }
24970
+ const cachedResult = barrelIndexModuleInfoCache.get(filePath);
24971
+ if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
24972
+ if (fileStat === null) return createNonBarrelInfo();
24973
+ let moduleInfo = createNonBarrelInfo();
24974
+ try {
24975
+ moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
24976
+ } catch {
24977
+ moduleInfo = createNonBarrelInfo();
24978
+ }
24979
+ barrelIndexModuleInfoCache.set(filePath, {
24980
+ mtimeMs: fileStat.mtimeMs,
24981
+ size: fileStat.size,
24982
+ moduleInfo
24983
+ });
24984
+ return moduleInfo;
24985
+ };
24986
+ const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
24987
+ //#endregion
24286
24988
  //#region src/react-native-dependency-names.ts
24287
24989
  const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
24288
24990
  "expo",
@@ -24485,6 +25187,35 @@ const classifyReactNativeFileTarget = (context) => {
24485
25187
  };
24486
25188
  const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
24487
25189
  //#endregion
25190
+ //#region src/plugin/utils/resolve-barrel-export-file-path.ts
25191
+ const getUniqueFilePath = (filePaths) => {
25192
+ const uniqueFilePaths = new Set(filePaths);
25193
+ if (uniqueFilePaths.size !== 1) return null;
25194
+ const [filePath] = uniqueFilePaths;
25195
+ return filePath ?? null;
25196
+ };
25197
+ const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
25198
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
25199
+ if (!resolvedTargetPath) return null;
25200
+ const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
25201
+ if (nestedTargetPath) return nestedTargetPath;
25202
+ return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
25203
+ };
25204
+ const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
25205
+ if (visitedFilePaths.has(barrelFilePath)) return null;
25206
+ visitedFilePaths.add(barrelFilePath);
25207
+ const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
25208
+ if (!moduleInfo.isBarrel) return null;
25209
+ const target = moduleInfo.exportsByName.get(exportedName);
25210
+ if (target) {
25211
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
25212
+ if (!resolvedTargetPath) return null;
25213
+ return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
25214
+ }
25215
+ if (exportedName === "default") return null;
25216
+ return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
25217
+ };
25218
+ //#endregion
24488
25219
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
24489
25220
  const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
24490
25221
  const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
@@ -24814,7 +25545,7 @@ const isAsyncFunctionLike = (node) => {
24814
25545
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
24815
25546
  return false;
24816
25547
  };
24817
- const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
25548
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
24818
25549
  "forEach",
24819
25550
  "map",
24820
25551
  "filter",
@@ -24835,7 +25566,7 @@ const runsOnEffectDispatch = (functionNode) => {
24835
25566
  if (parent.callee === functionNode) return true;
24836
25567
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
24837
25568
  const callee = parent.callee;
24838
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
25569
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
24839
25570
  };
24840
25571
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
24841
25572
  const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
@@ -25779,57 +26510,6 @@ const noDefaultProps = defineRule({
25779
26510
  } })
25780
26511
  });
25781
26512
  //#endregion
25782
- //#region src/plugin/rules/state-and-effects/no-derived-state.ts
25783
- const getStateName$1 = (stateDeclarator) => {
25784
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
25785
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
25786
- const stateBinding = stateDeclarator.id.elements?.[0];
25787
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
25788
- const setterBinding = stateDeclarator.id.elements?.[1];
25789
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
25790
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
25791
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
25792
- };
25793
- const noDerivedState = defineRule({
25794
- id: "no-derived-state",
25795
- title: "Derived value copied into state",
25796
- severity: "warn",
25797
- tags: ["test-noise"],
25798
- 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",
25799
- create: (context) => ({ CallExpression(node) {
25800
- if (!isUseEffect(node)) return;
25801
- const analysis = getProgramAnalysis(node);
25802
- if (!analysis) return;
25803
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
25804
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
25805
- const stateName = getStateName$1(fact.stateDeclarator);
25806
- context.report({
25807
- node: fact.callExpression,
25808
- message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
25809
- });
25810
- }
25811
- } })
25812
- });
25813
- //#endregion
25814
- //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
25815
- const noDerivedStateEffect = defineRule({
25816
- id: "no-derived-state-effect",
25817
- title: "Derived state stored in an effect",
25818
- severity: "warn",
25819
- tags: ["test-noise"],
25820
- 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",
25821
- create: (context) => ({ CallExpression(node) {
25822
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25823
- const analysis = getProgramAnalysis(node);
25824
- if (!analysis) return;
25825
- if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
25826
- context.report({
25827
- node,
25828
- message: "You pay an extra render for state you can derive from other values."
25829
- });
25830
- } })
25831
- });
25832
- //#endregion
25833
26513
  //#region src/plugin/utils/is-component-function.ts
25834
26514
  const isFunctionAssignedToComponent = (functionNode) => {
25835
26515
  let cursor = functionNode.parent ?? null;
@@ -25963,6 +26643,236 @@ const createComponentPropStackTracker = (callbacks) => {
25963
26643
  };
25964
26644
  };
25965
26645
  //#endregion
26646
+ //#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
26647
+ const findReferenceDeclarator = (reference) => {
26648
+ for (const definition of reference.resolved?.defs ?? []) {
26649
+ const definitionNode = definition.node;
26650
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
26651
+ }
26652
+ return null;
26653
+ };
26654
+ const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
26655
+ const node = stripParenExpression(expression);
26656
+ if (!isNodeOfType(node, "Identifier")) return null;
26657
+ const reference = getRef(analysis, node);
26658
+ if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
26659
+ if (isProp(analysis, reference)) {
26660
+ if (isWholePropsObjectReference(analysis, reference)) return null;
26661
+ return { bindingIdentity: reference.resolved };
26662
+ }
26663
+ if (isState(analysis, reference)) {
26664
+ const stateDeclarator = getUseStateDecl(analysis, reference);
26665
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
26666
+ return { bindingIdentity: reference.resolved };
26667
+ }
26668
+ if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
26669
+ const declarator = findReferenceDeclarator(reference);
26670
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
26671
+ const nextVisitedBindings = new Set(visitedBindings);
26672
+ nextVisitedBindings.add(reference.resolved);
26673
+ return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
26674
+ };
26675
+ const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
26676
+ const getDirectBranchStatements = (branch) => {
26677
+ if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
26678
+ return [branch];
26679
+ };
26680
+ const getDirectCallExpression = (statement) => {
26681
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26682
+ const expression = stripParenExpression(statement.expression);
26683
+ return isNodeOfType(expression, "CallExpression") ? expression : null;
26684
+ };
26685
+ const getDirectAssignmentExpression = (statement) => {
26686
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26687
+ const expression = stripParenExpression(statement.expression);
26688
+ return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
26689
+ };
26690
+ const findDirectStateSetterReference = (analysis, callExpression) => {
26691
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
26692
+ const callee = stripParenExpression(callExpression.callee);
26693
+ if (!isNodeOfType(callee, "Identifier")) return null;
26694
+ const reference = getRef(analysis, callee);
26695
+ return reference && isStateSetter(analysis, reference) ? reference : null;
26696
+ };
26697
+ const getStaticRefCurrentReference = (analysis, expression) => {
26698
+ const node = stripParenExpression(expression);
26699
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
26700
+ const reference = getRef(analysis, node.object);
26701
+ if (!reference) return null;
26702
+ const declarator = findReferenceDeclarator(reference);
26703
+ return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
26704
+ };
26705
+ const getStateTracker = (analysis, expression) => {
26706
+ const node = stripParenExpression(expression);
26707
+ if (!isNodeOfType(node, "Identifier")) return null;
26708
+ const reference = getRef(analysis, node);
26709
+ if (!reference || !isState(analysis, reference)) return null;
26710
+ const declarator = getUseStateDecl(analysis, reference);
26711
+ if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
26712
+ return {
26713
+ kind: "state",
26714
+ declarator
26715
+ };
26716
+ };
26717
+ const getRefTracker = (analysis, expression) => {
26718
+ const reference = getStaticRefCurrentReference(analysis, expression);
26719
+ return reference ? {
26720
+ kind: "ref",
26721
+ reference
26722
+ } : null;
26723
+ };
26724
+ const getTrackerInitializer = (tracker) => {
26725
+ const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
26726
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
26727
+ const initializer = declarator.init.arguments?.[0];
26728
+ return initializer ? initializer : null;
26729
+ };
26730
+ const isTrackerSynchronized = (analysis, guard) => {
26731
+ for (const statement of guard.statements) {
26732
+ if (guard.tracker.kind === "state") {
26733
+ const callExpression = getDirectCallExpression(statement);
26734
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26735
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26736
+ if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
26737
+ const writtenValue = callExpression.arguments?.[0];
26738
+ if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
26739
+ continue;
26740
+ }
26741
+ const assignmentExpression = getDirectAssignmentExpression(statement);
26742
+ if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
26743
+ if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
26744
+ if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
26745
+ }
26746
+ return false;
26747
+ };
26748
+ const parseRenderTrackerGuard = (analysis, statement) => {
26749
+ if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
26750
+ const left = statement.test.left;
26751
+ const right = statement.test.right;
26752
+ const candidates = [{
26753
+ sourceExpression: left,
26754
+ trackerExpression: right
26755
+ }, {
26756
+ sourceExpression: right,
26757
+ trackerExpression: left
26758
+ }];
26759
+ for (const candidate of candidates) {
26760
+ const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
26761
+ if (!source) continue;
26762
+ const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
26763
+ if (!tracker) continue;
26764
+ const trackerInitializer = getTrackerInitializer(tracker);
26765
+ if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
26766
+ const guard = {
26767
+ source,
26768
+ tracker,
26769
+ statements: getDirectBranchStatements(statement.consequent)
26770
+ };
26771
+ return isTrackerSynchronized(analysis, guard) ? guard : null;
26772
+ }
26773
+ return null;
26774
+ };
26775
+ const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
26776
+ if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
26777
+ const references = setterReference.resolved.references.filter((reference) => !reference.init);
26778
+ if (references.length !== 1) return false;
26779
+ return references[0].identifier === callExpression.callee;
26780
+ };
26781
+ const getStateInitializer = (stateDeclarator) => {
26782
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
26783
+ const initializer = stateDeclarator.init.arguments?.[0];
26784
+ return initializer ? initializer : null;
26785
+ };
26786
+ const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
26787
+ if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
26788
+ const componentFunction = componentBody.parent;
26789
+ const facts = [];
26790
+ for (const statement of componentBody.body ?? []) {
26791
+ const guard = parseRenderTrackerGuard(analysis, statement);
26792
+ if (!guard) continue;
26793
+ for (const branchStatement of guard.statements) {
26794
+ const callExpression = getDirectCallExpression(branchStatement);
26795
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26796
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26797
+ if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
26798
+ const stateDeclarator = getUseStateDecl(analysis, setterReference);
26799
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
26800
+ const writtenValue = callExpression.arguments?.[0];
26801
+ const initializer = getStateInitializer(stateDeclarator);
26802
+ if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
26803
+ facts.push({
26804
+ callExpression,
26805
+ stateDeclarator
26806
+ });
26807
+ }
26808
+ }
26809
+ return facts;
26810
+ };
26811
+ //#endregion
26812
+ //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26813
+ const getStateName$1 = (stateDeclarator) => {
26814
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26815
+ if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26816
+ const stateBinding = stateDeclarator.id.elements?.[0];
26817
+ if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26818
+ const setterBinding = stateDeclarator.id.elements?.[1];
26819
+ if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26820
+ if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26821
+ return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26822
+ };
26823
+ const noDerivedState = defineRule({
26824
+ id: "no-derived-state",
26825
+ title: "Derived value copied into state",
26826
+ severity: "warn",
26827
+ tags: ["test-noise"],
26828
+ 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",
26829
+ create: (context) => {
26830
+ const reportStateWrite = (callExpression, stateDeclarator) => {
26831
+ const stateName = getStateName$1(stateDeclarator);
26832
+ context.report({
26833
+ node: callExpression,
26834
+ message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26835
+ });
26836
+ };
26837
+ return {
26838
+ ...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
26839
+ if (!componentBody) return;
26840
+ const analysis = getProgramAnalysis(componentBody);
26841
+ if (!analysis) return;
26842
+ for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
26843
+ } }).visitors,
26844
+ CallExpression(node) {
26845
+ if (!isUseEffect(node)) return;
26846
+ const analysis = getProgramAnalysis(node);
26847
+ if (!analysis) return;
26848
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26849
+ if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26850
+ reportStateWrite(fact.callExpression, fact.stateDeclarator);
26851
+ }
26852
+ }
26853
+ };
26854
+ }
26855
+ });
26856
+ //#endregion
26857
+ //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26858
+ const noDerivedStateEffect = defineRule({
26859
+ id: "no-derived-state-effect",
26860
+ title: "Derived state stored in an effect",
26861
+ severity: "warn",
26862
+ tags: ["test-noise"],
26863
+ 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",
26864
+ create: (context) => ({ CallExpression(node) {
26865
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26866
+ const analysis = getProgramAnalysis(node);
26867
+ if (!analysis) return;
26868
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26869
+ context.report({
26870
+ node,
26871
+ message: "You pay an extra render for state you can derive from other values."
26872
+ });
26873
+ } })
26874
+ });
26875
+ //#endregion
25966
26876
  //#region src/plugin/utils/is-initial-only-prop-name.ts
25967
26877
  const isInitialOnlyPropName = (propName) => {
25968
26878
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -27468,14 +28378,14 @@ const hasDocumentClassListMutation = (node) => {
27468
28378
  //#endregion
27469
28379
  //#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
27470
28380
  const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
27471
- const unwrapChainExpression$1 = (node) => {
28381
+ const unwrapChainExpression$2 = (node) => {
27472
28382
  if (!node) return null;
27473
28383
  if (isNodeOfType(node, "ChainExpression")) return node.expression;
27474
28384
  return node;
27475
28385
  };
27476
28386
  const collectGuardExpressions = (node, into) => {
27477
28387
  if (!node) return;
27478
- const unwrappedNode = unwrapChainExpression$1(node);
28388
+ const unwrappedNode = unwrapChainExpression$2(node);
27479
28389
  if (!unwrappedNode) return;
27480
28390
  const rootIdentifierName = getRootIdentifierName(unwrappedNode);
27481
28391
  if (rootIdentifierName) {
@@ -27506,7 +28416,7 @@ const isReturnOnlyStatement = (node) => {
27506
28416
  };
27507
28417
  const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
27508
28418
  const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
27509
- const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
28419
+ const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
27510
28420
  if (!unwrappedDependencyExpression) return false;
27511
28421
  if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
27512
28422
  return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
@@ -29565,6 +30475,405 @@ const noGrayOnColoredBackground = defineRule({
29565
30475
  } })
29566
30476
  });
29567
30477
  //#endregion
30478
+ //#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
30479
+ const findEnclosingJsxOpeningElement = (node) => {
30480
+ let cursor = node.parent;
30481
+ while (cursor) {
30482
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30483
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30484
+ cursor = cursor.parent ?? null;
30485
+ }
30486
+ return null;
30487
+ };
30488
+ //#endregion
30489
+ //#region src/plugin/utils/has-client-render-evidence.ts
30490
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30491
+ if (fileHasUseClientDirective) return true;
30492
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30493
+ if (displayName && isReactHookName(displayName)) return true;
30494
+ let callsHook = false;
30495
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30496
+ if (callsHook) return false;
30497
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30498
+ callsHook = true;
30499
+ return false;
30500
+ }
30501
+ });
30502
+ return callsHook;
30503
+ };
30504
+ //#endregion
30505
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
30506
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
30507
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
30508
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
30509
+ return false;
30510
+ };
30511
+ //#endregion
30512
+ //#region src/plugin/utils/read-initial-state-boolean.ts
30513
+ const readLogicalResult = (operator, leftResult, rightResult) => {
30514
+ if (operator === "&&") {
30515
+ if (leftResult === false || rightResult === false) return false;
30516
+ if (leftResult === true && rightResult === true) return true;
30517
+ return null;
30518
+ }
30519
+ if (leftResult === true || rightResult === true) return true;
30520
+ if (leftResult === false && rightResult === false) return false;
30521
+ return null;
30522
+ };
30523
+ const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
30524
+ if (!isNodeOfType(identifier, "Identifier")) return null;
30525
+ if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
30526
+ const binding = findVariableInitializer(identifier, identifier.name);
30527
+ if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
30528
+ visitedBindings.add(binding.bindingIdentifier);
30529
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
30530
+ if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
30531
+ const initializer = stripParenExpression(declarator.init);
30532
+ if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
30533
+ const initialState = initializer.arguments?.[0];
30534
+ if (!initialState) return false;
30535
+ if (initialState.type === "SpreadElement") return null;
30536
+ return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
30537
+ }
30538
+ const declaration = declarator.parent;
30539
+ if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
30540
+ return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
30541
+ };
30542
+ const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
30543
+ const unwrappedExpression = stripParenExpression(expression);
30544
+ if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
30545
+ if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
30546
+ if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
30547
+ if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
30548
+ if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
30549
+ if (unwrappedExpression.body.body.length !== 1) return null;
30550
+ const returnStatement = unwrappedExpression.body.body[0];
30551
+ if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
30552
+ return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
30553
+ }
30554
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
30555
+ const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
30556
+ return argumentResult === null ? null : !argumentResult;
30557
+ }
30558
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression") && (unwrappedExpression.operator === "&&" || unwrappedExpression.operator === "||")) return readLogicalResult(unwrappedExpression.operator, readInitialStateBooleanInternal(unwrappedExpression.left, scopes, new Set(visitedBindings), false), readInitialStateBooleanInternal(unwrappedExpression.right, scopes, new Set(visitedBindings), false));
30559
+ return null;
30560
+ };
30561
+ const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
30562
+ //#endregion
30563
+ //#region src/plugin/utils/is-after-client-only-early-return.ts
30564
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
30565
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30566
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30567
+ const ancestors = /* @__PURE__ */ new Set();
30568
+ let currentNode = node;
30569
+ while (currentNode) {
30570
+ ancestors.add(currentNode);
30571
+ currentNode = currentNode.parent ?? null;
30572
+ }
30573
+ for (const statement of body.body ?? []) {
30574
+ if (ancestors.has(statement)) return false;
30575
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30576
+ const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
30577
+ if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
30578
+ if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
30579
+ }
30580
+ return false;
30581
+ };
30582
+ //#endregion
30583
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
30584
+ const isGatedByFalsyInitialState = (node, scopes) => {
30585
+ let cursor = node;
30586
+ let parent = node.parent;
30587
+ while (parent) {
30588
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
30589
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
30590
+ if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
30591
+ cursor = parent;
30592
+ parent = parent.parent ?? null;
30593
+ }
30594
+ return false;
30595
+ };
30596
+ //#endregion
30597
+ //#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
30598
+ const evaluateEquality = (operator, left, right) => {
30599
+ if (operator === "===" || operator === "==") return left === right;
30600
+ if (operator === "!==" || operator === "!=") return left !== right;
30601
+ return null;
30602
+ };
30603
+ const readTypeofBrowserGlobal = (expression, context) => {
30604
+ const unwrappedExpression = stripParenExpression(expression);
30605
+ if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
30606
+ const argument = stripParenExpression(unwrappedExpression.argument);
30607
+ if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
30608
+ if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || argument.property.name !== "window" && argument.property.name !== "document") return null;
30609
+ return argument.property.name;
30610
+ };
30611
+ const matchBrowserPredicate = (expression, context) => {
30612
+ const unwrappedExpression = stripParenExpression(expression);
30613
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
30614
+ const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
30615
+ return innerMatch ? {
30616
+ browserGlobalName: innerMatch.browserGlobalName,
30617
+ clientResult: !innerMatch.clientResult,
30618
+ serverResult: !innerMatch.serverResult
30619
+ } : null;
30620
+ }
30621
+ if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
30622
+ const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
30623
+ const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
30624
+ const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
30625
+ const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
30626
+ const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
30627
+ const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
30628
+ if (!browserGlobalName || !comparedType) return null;
30629
+ const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
30630
+ const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
30631
+ if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
30632
+ return {
30633
+ browserGlobalName,
30634
+ clientResult,
30635
+ serverResult
30636
+ };
30637
+ };
30638
+ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
30639
+ if (operator === "&&") {
30640
+ if (leftResult === false || rightResult === false) return false;
30641
+ if (leftResult === true && rightResult === true) return true;
30642
+ return null;
30643
+ }
30644
+ if (leftResult === true || rightResult === true) return true;
30645
+ if (leftResult === false && rightResult === false) return false;
30646
+ return null;
30647
+ };
30648
+ const readHydrationConditionResult = (expression, context, runtime) => {
30649
+ const unwrappedExpression = stripParenExpression(expression);
30650
+ const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
30651
+ if (predicateMatch) return predicateMatch[`${runtime}Result`];
30652
+ const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
30653
+ if (staticResult !== null) return staticResult;
30654
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
30655
+ const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
30656
+ return argumentResult === null ? null : !argumentResult;
30657
+ }
30658
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
30659
+ return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
30660
+ };
30661
+ const matchHydrationCondition = (expression, context) => {
30662
+ const unwrappedExpression = stripParenExpression(expression);
30663
+ const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
30664
+ if (predicateMatch) return {
30665
+ predicateMatch,
30666
+ predicateNode: unwrappedExpression
30667
+ };
30668
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
30669
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
30670
+ const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
30671
+ const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
30672
+ if (leftMatch && rightMatch) {
30673
+ const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
30674
+ const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
30675
+ return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
30676
+ }
30677
+ const nestedMatch = leftMatch ?? rightMatch;
30678
+ if (!nestedMatch) return null;
30679
+ const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
30680
+ if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
30681
+ return nestedMatch;
30682
+ };
30683
+ const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
30684
+ const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
30685
+ if (!leftNode || !rightNode) return leftNode === rightNode;
30686
+ const left = stripParenExpression(leftNode);
30687
+ const right = stripParenExpression(rightNode);
30688
+ if (areExpressionsStructurallyEqual(left, right)) return true;
30689
+ if (left.type !== right.type) return false;
30690
+ if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
30691
+ if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
30692
+ if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
30693
+ return areRenderedBranchesEquivalent(left.expression, right.expression);
30694
+ }
30695
+ if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
30696
+ if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
30697
+ if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
30698
+ return areNodeArraysEquivalent(left.children, right.children);
30699
+ }
30700
+ if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
30701
+ if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
30702
+ if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
30703
+ return areRenderedBranchesEquivalent(left.value, right.value);
30704
+ }
30705
+ if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
30706
+ if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
30707
+ if (left.quasis.length !== right.quasis.length) return false;
30708
+ if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
30709
+ return areNodeArraysEquivalent(left.expressions, right.expressions);
30710
+ }
30711
+ return false;
30712
+ };
30713
+ const isRenderedValue = (node) => {
30714
+ const unwrappedNode = stripParenExpression(node);
30715
+ if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
30716
+ if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
30717
+ return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
30718
+ };
30719
+ const findRenderedValueInAndBranch = (node) => {
30720
+ const unwrappedNode = stripParenExpression(node);
30721
+ if (isRenderedValue(unwrappedNode)) return unwrappedNode;
30722
+ if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
30723
+ return findRenderedValueInAndBranch(unwrappedNode.right);
30724
+ };
30725
+ const findEnclosingJsxAttribute = (node) => {
30726
+ let currentNode = node.parent;
30727
+ while (currentNode) {
30728
+ if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
30729
+ if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
30730
+ currentNode = currentNode.parent;
30731
+ }
30732
+ return null;
30733
+ };
30734
+ const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
30735
+ let currentNode = node;
30736
+ let parentNode = currentNode.parent;
30737
+ while (parentNode) {
30738
+ if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
30739
+ const attribute = findEnclosingJsxAttribute(parentNode);
30740
+ return attribute ? !isEventHandlerAttribute(attribute) : true;
30741
+ }
30742
+ if (isNodeOfType(parentNode, "ReturnStatement")) {
30743
+ if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
30744
+ }
30745
+ if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
30746
+ if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
30747
+ currentNode = parentNode;
30748
+ parentNode = currentNode.parent;
30749
+ }
30750
+ return false;
30751
+ };
30752
+ const getReturnedValues = (statement) => {
30753
+ if (!statement) return [];
30754
+ if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
30755
+ if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
30756
+ if (!isNodeOfType(statement, "BlockStatement")) return [];
30757
+ const returnedValues = [];
30758
+ for (const childStatement of statement.body) {
30759
+ returnedValues.push(...getReturnedValues(childStatement));
30760
+ if (statementAlwaysExits(childStatement)) break;
30761
+ }
30762
+ return returnedValues;
30763
+ };
30764
+ const findFollowingReturnedValues = (ifStatement) => {
30765
+ const parentNode = ifStatement.parent;
30766
+ if (!isNodeOfType(parentNode, "BlockStatement")) return [];
30767
+ const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
30768
+ if (statementIndex < 0) return [];
30769
+ const returnedValues = [];
30770
+ for (const statement of parentNode.body.slice(statementIndex + 1)) {
30771
+ returnedValues.push(...getReturnedValues(statement));
30772
+ if (statementAlwaysExits(statement)) break;
30773
+ }
30774
+ return returnedValues;
30775
+ };
30776
+ const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
30777
+ const left = stripParenExpression(leftExpression);
30778
+ const right = stripParenExpression(rightExpression);
30779
+ if (areExpressionsStructurallyEqual(left, right)) return true;
30780
+ if (left.type !== right.type) return false;
30781
+ if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
30782
+ if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
30783
+ if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
30784
+ return false;
30785
+ };
30786
+ const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
30787
+ if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
30788
+ if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
30789
+ if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
30790
+ if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
30791
+ const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
30792
+ const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
30793
+ return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
30794
+ };
30795
+ const isStructuralRenderedValue = (node) => {
30796
+ if (!node) return false;
30797
+ const unwrappedNode = stripParenExpression(node);
30798
+ return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
30799
+ };
30800
+ const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
30801
+ if (!rightBranch) return false;
30802
+ const left = stripParenExpression(leftBranch);
30803
+ const right = stripParenExpression(rightBranch);
30804
+ return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
30805
+ };
30806
+ const noHydrationBranchOnBrowserGlobal = defineRule({
30807
+ id: "no-hydration-branch-on-browser-global",
30808
+ title: "Server and client render different branches",
30809
+ severity: "error",
30810
+ category: "Correctness",
30811
+ requires: ["ssr"],
30812
+ recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
30813
+ create: (context) => {
30814
+ if (isTestlikeFilename(context.filename)) return {};
30815
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30816
+ let fileHasUseClientDirective = false;
30817
+ let fileIsEmailTemplate = false;
30818
+ const reportedNodes = /* @__PURE__ */ new Set();
30819
+ const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
30820
+ const conditionMatch = matchHydrationCondition(conditionNode, context);
30821
+ if (!conditionMatch) return;
30822
+ const { predicateMatch, predicateNode } = conditionMatch;
30823
+ if (reportedNodes.has(predicateNode)) return;
30824
+ if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
30825
+ const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
30826
+ if (!componentOrHookNode) return;
30827
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30828
+ if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
30829
+ if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
30830
+ const attribute = findEnclosingJsxAttribute(predicateNode);
30831
+ if (!attribute || isEventHandlerAttribute(attribute)) return;
30832
+ }
30833
+ if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
30834
+ if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
30835
+ const openingElement = findEnclosingJsxOpeningElement(predicateNode);
30836
+ if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
30837
+ if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
30838
+ if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
30839
+ reportedNodes.add(predicateNode);
30840
+ context.report({
30841
+ node: predicateNode,
30842
+ message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
30843
+ });
30844
+ };
30845
+ return {
30846
+ Program(node) {
30847
+ fileHasUseClientDirective = hasDirective(node, "use client");
30848
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30849
+ },
30850
+ ConditionalExpression(node) {
30851
+ reportHydrationBranch(node.test, node.consequent, node.alternate, true);
30852
+ },
30853
+ LogicalExpression(node) {
30854
+ if (node.operator !== "&&" && node.operator !== "||") return;
30855
+ const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
30856
+ if (!renderedValue) return;
30857
+ reportHydrationBranch(node, renderedValue, null, true);
30858
+ },
30859
+ IfStatement(node) {
30860
+ if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
30861
+ const consequentValues = getReturnedValues(node.consequent);
30862
+ const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
30863
+ if (consequentValues.length === 0 || alternateValues.length === 0) return;
30864
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
30865
+ if (!componentOrHookNode) return;
30866
+ const enclosingFunction = findEnclosingFunction(node);
30867
+ if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
30868
+ for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
30869
+ if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
30870
+ reportHydrationBranch(node.test, consequentValue, alternateValue, false);
30871
+ }
30872
+ }
30873
+ };
30874
+ }
30875
+ });
30876
+ //#endregion
29568
30877
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
29569
30878
  const MESSAGE$22 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
29570
30879
  const noImgLazyWithHighFetchpriority = defineRule({
@@ -29585,6 +30894,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
29585
30894
  } })
29586
30895
  });
29587
30896
  //#endregion
30897
+ //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
30898
+ const TIMER_FUNCTION_NAMES = new Set([
30899
+ "cancelAnimationFrame",
30900
+ "clearInterval",
30901
+ "clearTimeout",
30902
+ "queueMicrotask",
30903
+ "requestAnimationFrame",
30904
+ "setInterval",
30905
+ "setTimeout"
30906
+ ]);
30907
+ const STORAGE_MUTATION_METHOD_NAMES = new Set([
30908
+ "clear",
30909
+ "removeItem",
30910
+ "setItem"
30911
+ ]);
30912
+ const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
30913
+ const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
30914
+ const NOTIFICATION_RECEIVER_NAMES = new Set([
30915
+ "message",
30916
+ "notification",
30917
+ "toast"
30918
+ ]);
30919
+ const NOTIFICATION_METHOD_NAMES = new Set([
30920
+ "error",
30921
+ "info",
30922
+ "loading",
30923
+ "open",
30924
+ "show",
30925
+ "success",
30926
+ "warning"
30927
+ ]);
30928
+ const getMemberCall = (node) => {
30929
+ if (!isNodeOfType(node, "CallExpression")) return null;
30930
+ if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
30931
+ if (!isNodeOfType(node.callee.property, "Identifier")) return null;
30932
+ return {
30933
+ methodName: node.callee.property.name,
30934
+ receiver: stripParenExpression(node.callee.object)
30935
+ };
30936
+ };
30937
+ const isNotificationReceiver = (receiver, scopes) => {
30938
+ if (!isNodeOfType(receiver, "Identifier")) return false;
30939
+ if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
30940
+ const symbol = scopes.symbolFor(receiver);
30941
+ if (symbol?.kind === "import") return true;
30942
+ if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
30943
+ const callee = symbol.initializer.callee;
30944
+ return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
30945
+ };
30946
+ const getKnownImpureCall = (callExpression, scopes) => {
30947
+ if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
30948
+ const memberCall = getMemberCall(callExpression);
30949
+ if (!memberCall) return null;
30950
+ const { methodName, receiver } = memberCall;
30951
+ if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
30952
+ if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
30953
+ if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
30954
+ return null;
30955
+ };
30956
+ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
30957
+ let rootIdentifier = null;
30958
+ if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
30959
+ else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
30960
+ if (!rootIdentifier) return null;
30961
+ const updaterScope = scopes.ownScopeFor(updater);
30962
+ if (!updaterScope) return null;
30963
+ const symbol = scopes.symbolFor(rootIdentifier);
30964
+ if (!symbol) return `the external value "${rootIdentifier.name}"`;
30965
+ if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
30966
+ return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
30967
+ };
30968
+ const findImpureUpdaterOperation = (updater, scopes) => {
30969
+ const analysis = getProgramAnalysis(updater);
30970
+ let operation = null;
30971
+ walkAst(updater, (child) => {
30972
+ if (operation) return false;
30973
+ if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
30974
+ if (isNodeOfType(child, "CallExpression")) {
30975
+ if (isNodeOfType(child.callee, "Identifier") && analysis) {
30976
+ const calleeReference = getRef(analysis, child.callee);
30977
+ if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
30978
+ operation = `the nested state update "${child.callee.name}()"`;
30979
+ return false;
30980
+ }
30981
+ }
30982
+ const impureCall = getKnownImpureCall(child, scopes);
30983
+ if (impureCall) {
30984
+ operation = impureCall;
30985
+ return false;
30986
+ }
30987
+ }
30988
+ if (isNodeOfType(child, "AssignmentExpression")) {
30989
+ operation = getExternalAssignmentDescription(child.left, updater, scopes);
30990
+ if (operation) return false;
30991
+ }
30992
+ if (isNodeOfType(child, "UpdateExpression")) {
30993
+ operation = getExternalAssignmentDescription(child.argument, updater, scopes);
30994
+ if (operation) return false;
30995
+ }
30996
+ });
30997
+ return operation;
30998
+ };
30999
+ const noImpureStateUpdater = defineRule({
31000
+ id: "no-impure-state-updater",
31001
+ title: "State updater has side effects",
31002
+ severity: "error",
31003
+ 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.",
31004
+ create: (context) => ({ CallExpression(node) {
31005
+ const updater = node.arguments?.[0];
31006
+ if (!updater || !isFunctionLike$1(updater)) return;
31007
+ const analysis = getProgramAnalysis(node);
31008
+ if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
31009
+ const calleeReference = getRef(analysis, node.callee);
31010
+ if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
31011
+ const stateDeclarator = getUseStateDecl(analysis, calleeReference);
31012
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
31013
+ const operation = findImpureUpdaterOperation(updater, context.scopes);
31014
+ if (!operation) return;
31015
+ context.report({
31016
+ node: updater,
31017
+ 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.`
31018
+ });
31019
+ } })
31020
+ });
31021
+ //#endregion
29588
31022
  //#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
29589
31023
  const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
29590
31024
  const REACT_USE_REF_OPTIONS = {
@@ -29751,7 +31185,7 @@ const noInitializeState = defineRule({
29751
31185
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
29752
31186
  const analysis = getProgramAnalysis(node);
29753
31187
  if (!analysis) return;
29754
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
31188
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
29755
31189
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
29756
31190
  const stateName = getStateName(fact.stateDeclarator);
29757
31191
  context.report({
@@ -30410,111 +31844,6 @@ const noLegacyContextApi = defineRule({
30410
31844
  }
30411
31845
  });
30412
31846
  //#endregion
30413
- //#region src/plugin/utils/executes-during-render.ts
30414
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
30415
- "map",
30416
- "filter",
30417
- "forEach",
30418
- "flatMap",
30419
- "reduce",
30420
- "reduceRight",
30421
- "some",
30422
- "every",
30423
- "find",
30424
- "findIndex",
30425
- "findLast",
30426
- "findLastIndex",
30427
- "sort",
30428
- "toSorted"
30429
- ]);
30430
- const executesDuringRender = (functionNode) => {
30431
- const parent = functionNode.parent;
30432
- if (!isNodeOfType(parent, "CallExpression")) return false;
30433
- if (parent.callee === functionNode) return true;
30434
- if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
30435
- 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;
30436
- };
30437
- //#endregion
30438
- //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
30439
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
30440
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
30441
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
30442
- return false;
30443
- };
30444
- //#endregion
30445
- //#region src/plugin/utils/find-declarator-for-binding.ts
30446
- const findDeclaratorForBinding = (bindingIdentifier) => {
30447
- let ancestor = bindingIdentifier.parent;
30448
- while (ancestor) {
30449
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
30450
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
30451
- ancestor = ancestor.parent ?? null;
30452
- }
30453
- return null;
30454
- };
30455
- //#endregion
30456
- //#region src/plugin/utils/references-falsy-initial-state.ts
30457
- const isFalsyLiteral = (node) => {
30458
- if (!node) return true;
30459
- if (isNodeOfType(node, "Literal")) return !node.value;
30460
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
30461
- };
30462
- const isFalsyInitialStateBinding = (identifier) => {
30463
- if (!isNodeOfType(identifier, "Identifier")) return false;
30464
- const binding = findVariableInitializer(identifier, identifier.name);
30465
- if (!binding) return false;
30466
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
30467
- if (!declarator?.init) return false;
30468
- const init = stripParenExpression(declarator.init);
30469
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
30470
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
30471
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
30472
- return isFalsyLiteral(init.arguments?.[0]);
30473
- };
30474
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
30475
- //#endregion
30476
- //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
30477
- const isGatedByFalsyInitialState = (node) => {
30478
- let cursor = node;
30479
- let parent = node.parent;
30480
- while (parent) {
30481
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
30482
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
30483
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
30484
- cursor = parent;
30485
- parent = parent.parent ?? null;
30486
- }
30487
- return false;
30488
- };
30489
- //#endregion
30490
- //#region src/plugin/utils/references-client-only-flag.ts
30491
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
30492
- const referencesClientOnlyFlag = (expression) => {
30493
- const unwrapped = stripParenExpression(expression);
30494
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
30495
- if (isNodeOfType(unwrapped, "MemberExpression")) {
30496
- const property = unwrapped.property;
30497
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
30498
- }
30499
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
30500
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
30501
- return false;
30502
- };
30503
- //#endregion
30504
- //#region src/plugin/utils/is-inside-client-only-guard.ts
30505
- const isInsideClientOnlyGuard = (node) => {
30506
- let cursor = node;
30507
- let parent = node.parent;
30508
- while (parent) {
30509
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
30510
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
30511
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
30512
- cursor = parent;
30513
- parent = parent.parent ?? null;
30514
- }
30515
- return false;
30516
- };
30517
- //#endregion
30518
31847
  //#region src/plugin/rules/performance/no-locale-format-in-render.ts
30519
31848
  const LOCALE_FORMAT_METHOD_NAMES = new Set([
30520
31849
  "toLocaleString",
@@ -30644,75 +31973,12 @@ const matchDateDefaultStringification = (node) => {
30644
31973
  }
30645
31974
  return null;
30646
31975
  };
30647
- const findRenderPhaseComponentOrHook = (node) => {
30648
- let functionNode = findEnclosingFunction(node);
30649
- while (functionNode) {
30650
- if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30651
- if (!executesDuringRender(functionNode)) return null;
30652
- functionNode = findEnclosingFunction(functionNode);
30653
- }
30654
- return null;
30655
- };
30656
- const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30657
- if (fileHasUseClientDirective) return true;
30658
- const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30659
- if (displayName && isReactHookName(displayName)) return true;
30660
- let callsHook = false;
30661
- walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30662
- if (callsHook) return false;
30663
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30664
- callsHook = true;
30665
- return false;
30666
- }
30667
- });
30668
- return callsHook;
30669
- };
30670
- const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30671
- const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30672
- if (!isNodeOfType(body, "BlockStatement")) return false;
30673
- const ancestors = /* @__PURE__ */ new Set();
30674
- let cursor = node;
30675
- while (cursor) {
30676
- ancestors.add(cursor);
30677
- cursor = cursor.parent ?? null;
30678
- }
30679
- for (const statement of body.body ?? []) {
30680
- if (ancestors.has(statement)) return false;
30681
- if (!isNodeOfType(statement, "IfStatement")) continue;
30682
- if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30683
- let returnsEarly = false;
30684
- walkAst(statement.consequent, (child) => {
30685
- if (isFunctionLike$1(child)) return false;
30686
- if (isNodeOfType(child, "ReturnStatement")) {
30687
- returnsEarly = true;
30688
- return false;
30689
- }
30690
- });
30691
- if (returnsEarly) return true;
30692
- }
30693
- return false;
30694
- };
30695
- const findEnclosingJsxOpeningElement = (node) => {
30696
- let cursor = node.parent;
30697
- while (cursor) {
30698
- if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30699
- if (isNodeOfType(cursor, "JSXFragment")) return null;
30700
- cursor = cursor.parent ?? null;
30701
- }
30702
- return null;
30703
- };
30704
31976
  const noLocaleFormatInRender = defineRule({
30705
31977
  id: "no-locale-format-in-render",
30706
31978
  title: "Locale/timezone formatting during render",
30707
31979
  severity: "warn",
30708
31980
  category: "Correctness",
30709
- disabledWhen: [
30710
- "vite",
30711
- "cra",
30712
- "expo",
30713
- "react-native",
30714
- "unknown"
30715
- ],
31981
+ requires: ["ssr"],
30716
31982
  recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
30717
31983
  create: (context) => {
30718
31984
  if (isTestlikeFilename(context.filename)) return {};
@@ -30722,13 +31988,12 @@ const noLocaleFormatInRender = defineRule({
30722
31988
  const reportedNodes = /* @__PURE__ */ new Set();
30723
31989
  const reportIfRenderPhase = (match) => {
30724
31990
  if (reportedNodes.has(match.node)) return;
30725
- const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
31991
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node, context.scopes);
30726
31992
  if (!componentOrHookNode) return;
30727
31993
  if (fileIsEmailTemplate) return;
30728
31994
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30729
- if (isInsideClientOnlyGuard(match.node)) return;
30730
- if (isGatedByFalsyInitialState(match.node)) return;
30731
- if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
31995
+ if (isGatedByFalsyInitialState(match.node, context.scopes)) return;
31996
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode, context.scopes)) return;
30732
31997
  if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30733
31998
  if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30734
31999
  reportedNodes.add(match.node);
@@ -30755,7 +32020,7 @@ const noLocaleFormatInRender = defineRule({
30755
32020
  if (!isNodeOfType(expression, "CallExpression")) return;
30756
32021
  if (!isNodeOfType(expression.callee, "Identifier")) return;
30757
32022
  const helperName = expression.callee.name;
30758
- const componentOrHookNode = findRenderPhaseComponentOrHook(node);
32023
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
30759
32024
  if (!componentOrHookNode) return;
30760
32025
  const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30761
32026
  if (!helperNode || !isFunctionLike$1(helperNode)) return;
@@ -30767,9 +32032,8 @@ const noLocaleFormatInRender = defineRule({
30767
32032
  if (!match || reportedNodes.has(match.node)) return;
30768
32033
  if (fileIsEmailTemplate) return;
30769
32034
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30770
- if (isInsideClientOnlyGuard(node)) return;
30771
- if (isGatedByFalsyInitialState(node)) return;
30772
- if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
32035
+ if (isGatedByFalsyInitialState(node, context.scopes)) return;
32036
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
30773
32037
  if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30774
32038
  reportedNodes.add(match.node);
30775
32039
  context.report({
@@ -30871,9 +32135,6 @@ const noLongTransitionDuration = defineRule({
30871
32135
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
30872
32136
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
30873
32137
  //#endregion
30874
- //#region src/plugin/utils/is-event-handler-attribute.ts
30875
- const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
30876
- //#endregion
30877
32138
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
30878
32139
  const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
30879
32140
  const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
@@ -31005,7 +32266,7 @@ const noMatchMediaInStateInitializer = defineRule({
31005
32266
  title: "matchMedia in state initializer",
31006
32267
  severity: "warn",
31007
32268
  category: "Correctness",
31008
- disabledWhen: ["vite", "cra"],
32269
+ requires: ["ssr"],
31009
32270
  recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
31010
32271
  create: (context) => {
31011
32272
  if (isTestlikeFilename(context.filename)) return {};
@@ -31556,6 +32817,41 @@ const noMutableInDeps = defineRule({
31556
32817
  }
31557
32818
  });
31558
32819
  //#endregion
32820
+ //#region src/plugin/utils/is-result-discarded-call.ts
32821
+ const isResultDiscardedCall = (callExpression) => {
32822
+ let node = callExpression;
32823
+ let parent = node.parent;
32824
+ while (parent) {
32825
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
32826
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
32827
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
32828
+ if (isNodeOfType(parent, "ChainExpression")) {
32829
+ node = parent;
32830
+ parent = node.parent;
32831
+ continue;
32832
+ }
32833
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
32834
+ node = parent;
32835
+ parent = node.parent;
32836
+ continue;
32837
+ }
32838
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
32839
+ node = parent;
32840
+ parent = node.parent;
32841
+ continue;
32842
+ }
32843
+ if (isNodeOfType(parent, "SequenceExpression")) {
32844
+ const expressions = parent.expressions ?? [];
32845
+ if (expressions[expressions.length - 1] !== node) return true;
32846
+ node = parent;
32847
+ parent = node.parent;
32848
+ continue;
32849
+ }
32850
+ return false;
32851
+ }
32852
+ return false;
32853
+ };
32854
+ //#endregion
31559
32855
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
31560
32856
  const LODASH_MUTATOR_NAMES = new Set([
31561
32857
  "set",
@@ -32846,7 +34142,7 @@ const isUseRefIdentifier = (identifier) => {
32846
34142
  };
32847
34143
  const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
32848
34144
  const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
32849
- const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
34145
+ const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
32850
34146
  const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
32851
34147
  "useCallback",
32852
34148
  "useMemo",
@@ -32881,7 +34177,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
32881
34177
  return Boolean(ref.resolved?.defs.some((def) => {
32882
34178
  const node = def.node;
32883
34179
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
32884
- const initializer = unwrapChainExpression(node.init);
34180
+ const initializer = unwrapChainExpression$1(node.init);
32885
34181
  const wrappedFunction = getWrapperHookWrappedFunction(initializer);
32886
34182
  if (wrappedFunction) {
32887
34183
  if (wrappedFunction.async) return false;
@@ -32891,10 +34187,170 @@ const isDirectParentCallbackRef = (analysis, ref) => {
32891
34187
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
32892
34188
  }));
32893
34189
  };
34190
+ const getDeclarationKind = (declarator) => {
34191
+ const declaration = declarator.parent;
34192
+ return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
34193
+ };
34194
+ const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
34195
+ const getDestructuredPropertyName = (bindingIdentifier) => {
34196
+ const property = bindingIdentifier.parent;
34197
+ if (!property || !isNodeOfType(property, "Property")) return null;
34198
+ if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
34199
+ return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
34200
+ };
34201
+ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
34202
+ const unwrappedExpression = stripParenExpression(expression);
34203
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
34204
+ const callbackReference = getRef(analysis, unwrappedExpression);
34205
+ const callbackVariable = callbackReference?.resolved;
34206
+ if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
34207
+ if (isProp(analysis, callbackReference)) {
34208
+ if (isWholePropsObjectReference(analysis, callbackReference)) return null;
34209
+ const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
34210
+ return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
34211
+ }
34212
+ if (hasMutableBindingWrite(callbackReference)) return null;
34213
+ const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34214
+ if (definitions.length !== 1) return null;
34215
+ const declarator = definitions[0];
34216
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
34217
+ visitedVariables.add(callbackVariable);
34218
+ if (isNodeOfType(declarator.id, "ObjectPattern")) {
34219
+ const bindingIdentifier = callbackVariable.defs[0]?.name;
34220
+ const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
34221
+ const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
34222
+ return propertyName && propsReference ? propertyName : null;
34223
+ }
34224
+ if (!isNodeOfType(declarator.id, "Identifier")) return null;
34225
+ return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
34226
+ }
34227
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
34228
+ const callbackName = getStaticMemberPropertyName(unwrappedExpression);
34229
+ if (!callbackName) return null;
34230
+ const receiver = stripParenExpression(unwrappedExpression.object);
34231
+ if (!isNodeOfType(receiver, "Identifier")) return null;
34232
+ const receiverReference = getRef(analysis, receiver);
34233
+ if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
34234
+ return callbackName;
34235
+ };
34236
+ const isNullishRefInitializer = (initializer) => {
34237
+ if (!initializer) return true;
34238
+ const unwrappedInitializer = stripParenExpression(initializer);
34239
+ if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
34240
+ if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
34241
+ return unwrappedInitializer.name === "undefined";
34242
+ };
34243
+ const getDirectComponentBodyStatement = (node, componentBody) => {
34244
+ let current = node;
34245
+ while (current?.parent && current.parent !== componentBody) current = current.parent;
34246
+ return current?.parent === componentBody ? current : null;
34247
+ };
34248
+ const getVariableForDeclarator = (analysis, declarator) => {
34249
+ for (const scope of analysis.scopeManager.scopes) {
34250
+ const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
34251
+ if (variable) return variable;
34252
+ }
34253
+ return null;
34254
+ };
34255
+ const getRefMember = (identifier) => {
34256
+ const receiver = findTransparentExpressionRoot(identifier);
34257
+ const member = receiver.parent;
34258
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
34259
+ return member;
34260
+ };
34261
+ const getRefMemberAssignment = (identifier) => {
34262
+ const member = getRefMember(identifier);
34263
+ if (!member) return null;
34264
+ const memberRoot = findTransparentExpressionRoot(member);
34265
+ const assignment = memberRoot.parent;
34266
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
34267
+ return assignment;
34268
+ };
34269
+ const getRefAliasDeclarator = (identifier) => {
34270
+ const initializer = findTransparentExpressionRoot(identifier);
34271
+ const declarator = initializer.parent;
34272
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
34273
+ return declarator;
34274
+ };
34275
+ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
34276
+ if (!isNodeOfType(receiver, "Identifier")) return null;
34277
+ const receiverReference = getRef(analysis, receiver);
34278
+ if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
34279
+ const variables = /* @__PURE__ */ new Set();
34280
+ let currentVariable = receiverReference.resolved;
34281
+ let refCall = null;
34282
+ while (currentVariable && !variables.has(currentVariable)) {
34283
+ variables.add(currentVariable);
34284
+ const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
34285
+ if (definitions.length !== 1) return null;
34286
+ const declarator = definitions[0];
34287
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
34288
+ if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
34289
+ refCall = declarator.init;
34290
+ break;
34291
+ }
34292
+ if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
34293
+ const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
34294
+ if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
34295
+ currentVariable = upstreamReference.resolved;
34296
+ }
34297
+ if (!refCall) return null;
34298
+ const pendingVariables = [...variables];
34299
+ while (pendingVariables.length > 0) {
34300
+ const variable = pendingVariables.pop();
34301
+ if (!variable) continue;
34302
+ for (const candidateReference of variable.references) {
34303
+ const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
34304
+ if (!aliasDeclarator) continue;
34305
+ const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
34306
+ if (!aliasVariable || variables.has(aliasVariable)) continue;
34307
+ if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
34308
+ variables.add(aliasVariable);
34309
+ pendingVariables.push(aliasVariable);
34310
+ }
34311
+ }
34312
+ return {
34313
+ refCall,
34314
+ variables
34315
+ };
34316
+ };
34317
+ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
34318
+ const callee = stripParenExpression(callExpression.callee);
34319
+ if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
34320
+ const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
34321
+ if (!bindingProvenance) return null;
34322
+ const { refCall, variables } = bindingProvenance;
34323
+ const componentFunction = findEnclosingFunction(effectCall);
34324
+ if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
34325
+ if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
34326
+ const callbackPropNames = /* @__PURE__ */ new Set();
34327
+ const initializer = refCall.arguments?.[0];
34328
+ const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
34329
+ if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
34330
+ else if (!isNullishRefInitializer(initializer)) return null;
34331
+ for (const variable of variables) for (const candidateReference of variable.references) {
34332
+ if (candidateReference.init) continue;
34333
+ if (candidateReference.isWrite()) return null;
34334
+ const identifier = candidateReference.identifier;
34335
+ if (getRefAliasDeclarator(identifier)) continue;
34336
+ const member = getRefMember(identifier);
34337
+ if (!member || getStaticMemberPropertyName(member) !== "current") return null;
34338
+ const memberParent = findTransparentExpressionRoot(member).parent;
34339
+ if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
34340
+ const assignment = getRefMemberAssignment(identifier);
34341
+ if (!assignment) continue;
34342
+ const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
34343
+ if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
34344
+ const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
34345
+ if (!assignedCallbackName) return null;
34346
+ callbackPropNames.add(assignedCallbackName);
34347
+ }
34348
+ return callbackPropNames.size > 0 ? { callbackPropNames } : null;
34349
+ };
32894
34350
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
32895
34351
  const node = def.node;
32896
34352
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
32897
- return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
34353
+ return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
32898
34354
  }));
32899
34355
  const isHandlerBagArgument = (analysis, argument) => {
32900
34356
  if (!isNodeOfType(argument, "ObjectExpression")) return false;
@@ -32916,7 +34372,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
32916
34372
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
32917
34373
  const node = def.node;
32918
34374
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
32919
- const init = unwrapChainExpression(node.init);
34375
+ const init = unwrapChainExpression$1(node.init);
32920
34376
  if (!isNodeOfType(init, "CallExpression")) return false;
32921
34377
  const callee = init.callee;
32922
34378
  if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
@@ -32946,71 +34402,79 @@ const noPassDataToParent = defineRule({
32946
34402
  severity: "warn",
32947
34403
  tags: ["test-noise"],
32948
34404
  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",
32949
- create: (context) => ({ CallExpression(node) {
32950
- if (!isUseEffect(node)) return;
32951
- const analysis = getProgramAnalysis(node);
32952
- if (!analysis) return;
32953
- if (hasCleanup(analysis, node)) return;
32954
- const effectFnRefs = getEffectFnRefs(analysis, node);
32955
- if (!effectFnRefs) return;
32956
- const effectFn = getEffectFn(analysis, node);
32957
- if (!effectFn) return;
32958
- for (const ref of effectFnRefs) {
32959
- const callExpr = getCallExpr(ref);
32960
- if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
32961
- if (isRefCall(analysis, ref)) continue;
32962
- if (!isSynchronous(ref.identifier, effectFn)) continue;
32963
- const calleeNode = unwrapChainExpression(callExpr.callee);
32964
- const identifier = ref.identifier;
32965
- if (calleeNode === identifier) {
32966
- if (!isDirectParentCallbackRef(analysis, ref)) continue;
32967
- if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
32968
- } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
32969
- if (!isWholePropsObjectReference(analysis, ref)) continue;
32970
- if (isCustomHookParameter(ref)) continue;
32971
- } else continue;
32972
- const methodName = getCallMethodName(calleeNode);
32973
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
32974
- if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
32975
- if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
32976
- if (isNamespacedApiCallee(calleeNode)) continue;
32977
- const calleeName = isNodeOfType(identifier, "Identifier") ? identifier.name : methodName;
32978
- const isSetterNamedCallee = Boolean(calleeName && SETTER_NAMED_PROP_PATTERN.test(calleeName));
32979
- const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
32980
- const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
32981
- if (isFunctionLike$1(argument)) {
32982
- if (!isSetterNamedCallee) return [];
32983
- return getFunctionalUpdaterDataRefs(analysis, argument);
32984
- }
32985
- if (isHandlerBagArgument(analysis, argument)) return [];
32986
- if (isParentWiredHookResultArgument(analysis, argument)) return [];
32987
- if (isNodeOfType(argument, "Identifier")) {
32988
- const argumentRef = getRef(analysis, argument);
32989
- if (argumentRef && resolveToFunction(argumentRef)) return [];
32990
- }
32991
- return getDownstreamRefs(analysis, argument);
32992
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
32993
- if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
32994
- if (!argsUpstreamRefs.some((argRef) => {
32995
- if (isUseStateIdentifier(argRef.identifier)) return false;
32996
- if (isProp(analysis, argRef)) return false;
32997
- if (isUseRefIdentifier(argRef.identifier)) return false;
32998
- if (isRefCurrent(argRef)) return false;
32999
- if (isConstant(argRef)) return false;
33000
- if (isParentWiredHookResultRef(analysis, argRef)) return false;
33001
- if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
33002
- if (resolvesToFunctionBinding(argRef)) return false;
33003
- const argIdentifier = argRef.identifier;
33004
- if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
33005
- if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
33006
- return true;
33007
- })) continue;
33008
- context.report({
33009
- node: callExpr,
33010
- message: "Handing data back to a parent from a useEffect costs your users an extra render."
33011
- });
33012
- }
33013
- } })
34405
+ create: (context) => {
34406
+ const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
34407
+ allowGlobalReactNamespace: true,
34408
+ allowUnboundBareCalls: true
34409
+ });
34410
+ return { CallExpression(node) {
34411
+ if (!isUseEffect(node)) return;
34412
+ const analysis = getProgramAnalysis(node);
34413
+ if (!analysis) return;
34414
+ if (hasCleanup(analysis, node)) return;
34415
+ const effectFnRefs = getEffectFnRefs(analysis, node);
34416
+ if (!effectFnRefs) return;
34417
+ const effectFn = getEffectFn(analysis, node);
34418
+ if (!effectFn) return;
34419
+ for (const ref of effectFnRefs) {
34420
+ const callExpr = getCallExpr(ref);
34421
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
34422
+ const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
34423
+ if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
34424
+ if (!isSynchronous(ref.identifier, effectFn)) continue;
34425
+ const calleeNode = unwrapChainExpression$1(callExpr.callee);
34426
+ const identifier = ref.identifier;
34427
+ if (callbackRefProvenance) {
34428
+ if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
34429
+ } else if (calleeNode === identifier) {
34430
+ if (!isDirectParentCallbackRef(analysis, ref)) continue;
34431
+ if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
34432
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
34433
+ if (!isWholePropsObjectReference(analysis, ref)) continue;
34434
+ if (isCustomHookParameter(ref)) continue;
34435
+ } else continue;
34436
+ const methodName = getCallMethodName(calleeNode);
34437
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
34438
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
34439
+ if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
34440
+ if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
34441
+ 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) ?? ""));
34442
+ const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
34443
+ const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
34444
+ if (isFunctionLike$1(argument)) {
34445
+ if (!isSetterNamedCallee) return [];
34446
+ return getFunctionalUpdaterDataRefs(analysis, argument);
34447
+ }
34448
+ if (isHandlerBagArgument(analysis, argument)) return [];
34449
+ if (isParentWiredHookResultArgument(analysis, argument)) return [];
34450
+ if (isNodeOfType(argument, "Identifier")) {
34451
+ const argumentRef = getRef(analysis, argument);
34452
+ if (argumentRef && resolveToFunction(argumentRef)) return [];
34453
+ }
34454
+ return getDownstreamRefs(analysis, argument);
34455
+ }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34456
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34457
+ if (!argsUpstreamRefs.some((argRef) => {
34458
+ if (isUseStateIdentifier(argRef.identifier)) return false;
34459
+ if (isProp(analysis, argRef)) return false;
34460
+ if (isUseRefIdentifier(argRef.identifier)) return false;
34461
+ if (isRefCurrent(argRef)) return false;
34462
+ if (isConstant(argRef)) return false;
34463
+ if (isParentWiredHookResultRef(analysis, argRef)) return false;
34464
+ if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
34465
+ if (resolvesToFunctionBinding(argRef)) return false;
34466
+ const argIdentifier = argRef.identifier;
34467
+ if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
34468
+ if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
34469
+ return true;
34470
+ })) continue;
34471
+ context.report({
34472
+ node: callExpr,
34473
+ message: "Handing data back to a parent from a useEffect costs your users an extra render."
34474
+ });
34475
+ }
34476
+ } };
34477
+ }
33014
34478
  });
33015
34479
  //#endregion
33016
34480
  //#region src/plugin/utils/is-call-result-consumed-as-argument.ts
@@ -34258,6 +35722,63 @@ const noRedundantShouldComponentUpdate = defineRule({
34258
35722
  }
34259
35723
  });
34260
35724
  //#endregion
35725
+ //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
35726
+ const resolveReactRefSymbol = (memberExpression, scopes) => {
35727
+ const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
35728
+ if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
35729
+ const symbol = resolveConstIdentifierAlias(receiver, scopes);
35730
+ if (!symbol?.initializer) return null;
35731
+ const initializer = stripParenExpression(symbol.initializer);
35732
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
35733
+ return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
35734
+ };
35735
+ const isSameRefCurrentMember = (node, refSymbol, scopes) => {
35736
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
35737
+ const receiver = stripParenExpression(node.object);
35738
+ return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
35739
+ };
35740
+ const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
35741
+ if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
35742
+ let descendant = assignmentExpression;
35743
+ let ancestor = descendant.parent;
35744
+ while (ancestor) {
35745
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
35746
+ const { left, right } = ancestor.test;
35747
+ if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
35748
+ }
35749
+ descendant = ancestor;
35750
+ ancestor = descendant.parent;
35751
+ }
35752
+ return false;
35753
+ };
35754
+ const noRefCurrentInRender = defineRule({
35755
+ id: "no-ref-current-in-render",
35756
+ title: "Ref mutated during render",
35757
+ severity: "error",
35758
+ 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.",
35759
+ create: (context) => {
35760
+ const report = (memberExpression) => {
35761
+ if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
35762
+ if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
35763
+ context.report({
35764
+ node: memberExpression,
35765
+ message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
35766
+ });
35767
+ };
35768
+ return {
35769
+ AssignmentExpression(node) {
35770
+ const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
35771
+ if (!refSymbol) return;
35772
+ if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
35773
+ report(node.left);
35774
+ },
35775
+ UpdateExpression(node) {
35776
+ report(node.argument);
35777
+ }
35778
+ };
35779
+ }
35780
+ });
35781
+ //#endregion
34261
35782
  //#region src/plugin/rules/architecture/no-render-in-render.ts
34262
35783
  const isInsideComponentContext = (node) => {
34263
35784
  let cursor = node.parent;
@@ -36272,6 +37793,140 @@ const noUnescapedEntities = defineRule({
36272
37793
  } })
36273
37794
  });
36274
37795
  //#endregion
37796
+ //#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
37797
+ const BROWSER_GLOBAL_NAMES = new Set([
37798
+ "window",
37799
+ "document",
37800
+ "localStorage",
37801
+ "sessionStorage",
37802
+ "navigator",
37803
+ "matchMedia"
37804
+ ]);
37805
+ const getTypeofBrowserGlobalName = (expression, context) => {
37806
+ const unwrappedExpression = stripParenExpression(expression);
37807
+ if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
37808
+ const argument = stripParenExpression(unwrappedExpression.argument);
37809
+ if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
37810
+ if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(argument.property.name)) return null;
37811
+ return argument.property.name;
37812
+ };
37813
+ const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
37814
+ const mergeAvailability = (leftAvailability, rightAvailability) => {
37815
+ if (leftAvailability === null) return rightAvailability;
37816
+ if (rightAvailability === null) return leftAvailability;
37817
+ return leftAvailability === rightAvailability ? leftAvailability : null;
37818
+ };
37819
+ const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
37820
+ const unwrappedExpression = stripParenExpression(expression);
37821
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
37822
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
37823
+ if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
37824
+ if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
37825
+ return null;
37826
+ }
37827
+ if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
37828
+ const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
37829
+ const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
37830
+ const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
37831
+ const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
37832
+ const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
37833
+ const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
37834
+ if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
37835
+ if (!comparedType) return null;
37836
+ const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
37837
+ const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
37838
+ if (!isEquality && !isInequality) return null;
37839
+ const browserType = guardName === "matchMedia" ? "function" : "object";
37840
+ const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
37841
+ if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
37842
+ return predicateResult === browserResult;
37843
+ };
37844
+ const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
37845
+ let currentNode = node;
37846
+ let parentNode = currentNode.parent;
37847
+ while (parentNode) {
37848
+ if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
37849
+ if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
37850
+ if (isNodeOfType(parentNode, "ConditionalExpression")) {
37851
+ if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
37852
+ }
37853
+ if (isNodeOfType(parentNode, "IfStatement")) {
37854
+ if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
37855
+ }
37856
+ currentNode = parentNode;
37857
+ parentNode = currentNode.parent;
37858
+ }
37859
+ return false;
37860
+ };
37861
+ const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
37862
+ const enclosingFunction = findEnclosingFunction(node);
37863
+ if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
37864
+ let currentNode = node;
37865
+ while (currentNode !== enclosingFunction) {
37866
+ const parentNode = currentNode.parent;
37867
+ if (!parentNode) return false;
37868
+ if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
37869
+ if (statement === currentNode) break;
37870
+ if (!isNodeOfType(statement, "IfStatement")) continue;
37871
+ if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
37872
+ if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
37873
+ }
37874
+ currentNode = parentNode;
37875
+ }
37876
+ return false;
37877
+ };
37878
+ const isTypeofProbe = (node) => {
37879
+ const expressionRoot = findTransparentExpressionRoot(node);
37880
+ const parentNode = expressionRoot.parent;
37881
+ return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
37882
+ };
37883
+ const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
37884
+ id: "no-unguarded-browser-global-in-render-or-hook-init",
37885
+ title: "Browser global read during server render",
37886
+ severity: "error",
37887
+ category: "Correctness",
37888
+ requires: ["ssr"],
37889
+ recommendation: "Move browser-only reads into an effect or event, guard them behind a client-only render path, or use useSyncExternalStore with a stable server snapshot.",
37890
+ create: (context) => {
37891
+ if (isTestlikeFilename(context.filename)) return {};
37892
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
37893
+ let fileIsEmailTemplate = false;
37894
+ const reportedNodes = /* @__PURE__ */ new Set();
37895
+ const reportBrowserRead = (node, browserGlobalName) => {
37896
+ if (reportedNodes.has(node) || isTypeofProbe(node)) return;
37897
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
37898
+ if (!componentOrHookNode) return;
37899
+ if (fileIsEmailTemplate) return;
37900
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
37901
+ if (isGatedByFalsyInitialState(node, context.scopes)) return;
37902
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
37903
+ if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
37904
+ if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
37905
+ reportedNodes.add(node);
37906
+ context.report({
37907
+ node,
37908
+ message: `\`${browserGlobalName}\` is read while React is rendering on the server, where browser globals are unavailable. Move the read into an effect or event, or provide a stable server snapshot.`
37909
+ });
37910
+ };
37911
+ return {
37912
+ Program(node) {
37913
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
37914
+ },
37915
+ Identifier(node) {
37916
+ if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
37917
+ if (!context.scopes.isGlobalReference(node)) return;
37918
+ reportBrowserRead(node, node.name);
37919
+ },
37920
+ MemberExpression(node) {
37921
+ if (node.computed) return;
37922
+ const objectNode = stripParenExpression(node.object);
37923
+ if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
37924
+ reportBrowserRead(node, node.property.name);
37925
+ }
37926
+ };
37927
+ }
37928
+ });
37929
+ //#endregion
36275
37930
  //#region src/plugin/constants/dom-aria-properties.ts
36276
37931
  const ARIA_PROPERTY_NAMES = new Set([
36277
37932
  "activedescendant",
@@ -40114,6 +41769,123 @@ const preferUseEffectEvent = defineRule({
40114
41769
  }
40115
41770
  });
40116
41771
  //#endregion
41772
+ //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
41773
+ const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
41774
+ "each",
41775
+ "every",
41776
+ "filter",
41777
+ "find",
41778
+ "findIndex",
41779
+ "findLast",
41780
+ "findLastIndex",
41781
+ "flatMap",
41782
+ "forEach",
41783
+ "map",
41784
+ "reduce",
41785
+ "reduceRight",
41786
+ "some",
41787
+ "sort",
41788
+ "toSorted"
41789
+ ]);
41790
+ const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
41791
+ "from",
41792
+ "fromAsync",
41793
+ "groupBy"
41794
+ ]);
41795
+ const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
41796
+ const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
41797
+ const isListenerRemovalViaNullHandler = (callNode) => {
41798
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
41799
+ const callee = unwrapChainExpression(callNode.callee);
41800
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
41801
+ };
41802
+ const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
41803
+ "off",
41804
+ "removeEventListener",
41805
+ "removeListener"
41806
+ ]);
41807
+ const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
41808
+ if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
41809
+ const handlerArgument = callNode.arguments?.[1];
41810
+ return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
41811
+ };
41812
+ const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41813
+ const callNode = unwrapChainExpression(node);
41814
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
41815
+ if (isListenerRemovalViaNullHandler(callNode)) return true;
41816
+ const callee = unwrapChainExpression(callNode.callee);
41817
+ if (isNodeOfType(callee, "Identifier")) {
41818
+ if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
41819
+ if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
41820
+ if (knownCleanupFunctionNames.has(callee.name)) return true;
41821
+ return false;
41822
+ }
41823
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
41824
+ if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
41825
+ if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
41826
+ return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
41827
+ }
41828
+ return false;
41829
+ };
41830
+ 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);
41831
+ const isIteratorCallbackArgument = (node) => {
41832
+ const parentNode = node.parent;
41833
+ if (!isNodeOfType(parentNode, "CallExpression")) return false;
41834
+ if (!parentNode.arguments?.some((argument) => argument === node)) return false;
41835
+ const callee = unwrapChainExpression(parentNode.callee);
41836
+ if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
41837
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
41838
+ };
41839
+ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41840
+ let didFindRelease = false;
41841
+ walkAst(node, (child) => {
41842
+ if (didFindRelease) return false;
41843
+ if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
41844
+ if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
41845
+ didFindRelease = true;
41846
+ return false;
41847
+ }
41848
+ });
41849
+ return didFindRelease;
41850
+ };
41851
+ const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41852
+ if (!isFunctionLike$1(node)) return false;
41853
+ return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
41854
+ };
41855
+ const isProvablyNoOpCleanupFunction = (node) => {
41856
+ if (!isFunctionLike$1(node)) return false;
41857
+ let sawNoOpRemoval = false;
41858
+ let sawOtherCall = false;
41859
+ walkAst(node.body, (child) => {
41860
+ if (sawOtherCall) return false;
41861
+ if (isFunctionLike$1(child)) return false;
41862
+ const callNode = unwrapChainExpression(child);
41863
+ if (!isNodeOfType(callNode, "CallExpression")) return;
41864
+ const callee = unwrapChainExpression(callNode.callee);
41865
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
41866
+ sawNoOpRemoval = true;
41867
+ return;
41868
+ }
41869
+ sawOtherCall = true;
41870
+ });
41871
+ return sawNoOpRemoval && !sawOtherCall;
41872
+ };
41873
+ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
41874
+ if (!returnedValue) return false;
41875
+ const unwrappedValue = unwrapChainExpression(returnedValue);
41876
+ if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
41877
+ if (isNodeOfType(unwrappedValue, "Identifier")) {
41878
+ if (unwrappedValue.name === "undefined") return false;
41879
+ if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
41880
+ return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
41881
+ }
41882
+ if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
41883
+ if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
41884
+ if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
41885
+ if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
41886
+ return false;
41887
+ };
41888
+ //#endregion
40117
41889
  //#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
40118
41890
  const findUseEffectsInComponent = (componentBody) => {
40119
41891
  const effectCalls = [];
@@ -41582,7 +43354,7 @@ const renderingHydrationMismatchTime = defineRule({
41582
43354
  title: "Time or random value in JSX",
41583
43355
  severity: "warn",
41584
43356
  category: "Correctness",
41585
- disabledWhen: ["vite", "cra"],
43357
+ requires: ["ssr"],
41586
43358
  recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
41587
43359
  create: (context) => {
41588
43360
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -41596,8 +43368,7 @@ const renderingHydrationMismatchTime = defineRule({
41596
43368
  const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
41597
43369
  if (matched) {
41598
43370
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
41599
- if (isInsideClientOnlyGuard(node)) return;
41600
- if (isGatedByFalsyInitialState(node)) return;
43371
+ if (isGatedByFalsyInitialState(node, context.scopes)) return;
41601
43372
  if (isInsideMotionTransitionAttribute(node)) return;
41602
43373
  context.report({
41603
43374
  node,
@@ -41606,11 +43377,10 @@ const renderingHydrationMismatchTime = defineRule({
41606
43377
  return;
41607
43378
  }
41608
43379
  walkAst(node.expression, (child) => {
41609
- if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
43380
+ if (isFunctionLike$1(child) && !executesDuringRender(child, context.scopes)) return false;
41610
43381
  for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
41611
43382
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
41612
- if (isInsideClientOnlyGuard(child)) return;
41613
- if (isGatedByFalsyInitialState(child)) return;
43383
+ if (isGatedByFalsyInitialState(child, context.scopes)) return;
41614
43384
  if (isInsideMotionTransitionAttribute(child)) return;
41615
43385
  if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
41616
43386
  context.report({
@@ -54947,6 +56717,18 @@ const reactDoctorRules = [
54947
56717
  category: "Accessibility"
54948
56718
  }
54949
56719
  },
56720
+ {
56721
+ key: "react-doctor/no-hydration-branch-on-browser-global",
56722
+ id: "no-hydration-branch-on-browser-global",
56723
+ source: "react-doctor",
56724
+ originallyExternal: false,
56725
+ rule: {
56726
+ ...noHydrationBranchOnBrowserGlobal,
56727
+ framework: "global",
56728
+ category: "Bugs",
56729
+ requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
56730
+ }
56731
+ },
54950
56732
  {
54951
56733
  key: "react-doctor/no-img-lazy-with-high-fetchpriority",
54952
56734
  id: "no-img-lazy-with-high-fetchpriority",
@@ -54959,6 +56741,18 @@ const reactDoctorRules = [
54959
56741
  requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
54960
56742
  }
54961
56743
  },
56744
+ {
56745
+ key: "react-doctor/no-impure-state-updater",
56746
+ id: "no-impure-state-updater",
56747
+ source: "react-doctor",
56748
+ originallyExternal: false,
56749
+ rule: {
56750
+ ...noImpureStateUpdater,
56751
+ framework: "global",
56752
+ category: "Bugs",
56753
+ requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
56754
+ }
56755
+ },
54962
56756
  {
54963
56757
  key: "react-doctor/no-indeterminate-attribute",
54964
56758
  id: "no-indeterminate-attribute",
@@ -55466,6 +57260,18 @@ const reactDoctorRules = [
55466
57260
  requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
55467
57261
  }
55468
57262
  },
57263
+ {
57264
+ key: "react-doctor/no-ref-current-in-render",
57265
+ id: "no-ref-current-in-render",
57266
+ source: "react-doctor",
57267
+ originallyExternal: false,
57268
+ rule: {
57269
+ ...noRefCurrentInRender,
57270
+ framework: "global",
57271
+ category: "Bugs",
57272
+ requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
57273
+ }
57274
+ },
55469
57275
  {
55470
57276
  key: "react-doctor/no-render-in-render",
55471
57277
  id: "no-render-in-render",
@@ -55710,6 +57516,18 @@ const reactDoctorRules = [
55710
57516
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
55711
57517
  }
55712
57518
  },
57519
+ {
57520
+ key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
57521
+ id: "no-unguarded-browser-global-in-render-or-hook-init",
57522
+ source: "react-doctor",
57523
+ originallyExternal: false,
57524
+ rule: {
57525
+ ...noUnguardedBrowserGlobalInRenderOrHookInit,
57526
+ framework: "global",
57527
+ category: "Bugs",
57528
+ requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
57529
+ }
57530
+ },
55713
57531
  {
55714
57532
  key: "react-doctor/no-unknown-property",
55715
57533
  id: "no-unknown-property",
@@ -58042,10 +59860,17 @@ const CROSS_FILE_RULE_IDS = new Set([
58042
59860
  "nextjs-no-use-search-params-without-suspense",
58043
59861
  "no-dynamic-import-path",
58044
59862
  "no-full-lodash-import",
59863
+ "no-hydration-branch-on-browser-global",
58045
59864
  "no-indeterminate-attribute",
58046
59865
  "no-locale-format-in-render",
58047
59866
  "no-match-media-in-state-initializer",
59867
+ "no-adjust-state-on-prop-change",
59868
+ "no-derived-state",
59869
+ "no-derived-state-effect",
59870
+ "no-event-handler",
59871
+ "no-initialize-state",
58048
59872
  "no-mutating-reducer-state",
59873
+ "no-unguarded-browser-global-in-render-or-hook-init",
58049
59874
  "prefer-dynamic-import",
58050
59875
  "rendering-hydration-mismatch-time",
58051
59876
  "rn-no-legacy-shadow-styles",
@@ -58097,6 +59922,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
58097
59922
  if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
58098
59923
  for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
58099
59924
  };
59925
+ const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
59926
+ for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
59927
+ };
58100
59928
  const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
58101
59929
  const namedUseReducerLocals = /* @__PURE__ */ new Set();
58102
59930
  const reactObjectLocals = /* @__PURE__ */ new Set();
@@ -58179,10 +60007,17 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
58179
60007
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
58180
60008
  ["no-dynamic-import-path", collectNearestManifestDependencies],
58181
60009
  ["no-full-lodash-import", collectNearestManifestDependencies],
60010
+ ["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
58182
60011
  ["no-indeterminate-attribute", collectNearestManifestDependencies],
58183
60012
  ["no-locale-format-in-render", collectNearestManifestDependencies],
58184
60013
  ["no-match-media-in-state-initializer", collectNearestManifestDependencies],
60014
+ ["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
60015
+ ["no-derived-state", collectEffectValueHelperDependencies],
60016
+ ["no-derived-state-effect", collectEffectValueHelperDependencies],
60017
+ ["no-event-handler", collectEffectValueHelperDependencies],
60018
+ ["no-initialize-state", collectEffectValueHelperDependencies],
58185
60019
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
60020
+ ["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
58186
60021
  ["prefer-dynamic-import", collectNearestManifestDependencies],
58187
60022
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
58188
60023
  ["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],