oxlint-plugin-react-doctor 0.7.4-dev.70b5d99 → 0.7.4-dev.98005f2

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 +139 -1
  2. package/dist/index.js +2316 -841
  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));
8848
9165
  };
8849
- const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8850
- const collectFileReleaseVerbNames = (anyNode) => {
8851
- let programNode = anyNode;
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);
9180
+ };
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;
8862
9193
  };
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;
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);
9211
+ };
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
  }
@@ -12339,20 +12738,6 @@ const collectHandlerReferencedNames = (root) => {
12339
12738
  return names;
12340
12739
  };
12341
12740
  //#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
12741
  //#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
12357
12742
  const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
12358
12743
  const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -13497,7 +13882,7 @@ const jsHoistIntl = defineRule({
13497
13882
  });
13498
13883
  //#endregion
13499
13884
  //#region src/plugin/utils/create-loop-aware-visitors.ts
13500
- const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
13885
+ const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
13501
13886
  "map",
13502
13887
  "flatMap",
13503
13888
  "forEach",
@@ -13516,7 +13901,7 @@ const isIteratorCallback = (node) => {
13516
13901
  const parent = node.parent;
13517
13902
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
13518
13903
  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);
13904
+ return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
13520
13905
  };
13521
13906
  const createLoopAwareVisitors = (innerVisitors, options = {}) => {
13522
13907
  let loopDepth = 0;
@@ -13745,7 +14130,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
13745
14130
  });
13746
14131
  return indexedArrayObject;
13747
14132
  };
13748
- const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
14133
+ const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
13749
14134
  const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
13750
14135
  const LENGTH_MISMATCH_OPERATORS = new Set([
13751
14136
  "!==",
@@ -13757,17 +14142,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
13757
14142
  ]);
13758
14143
  const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
13759
14144
  const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
13760
- const binaryGuard = unwrapChainExpression$2(candidate);
14145
+ const binaryGuard = unwrapChainExpression$3(candidate);
13761
14146
  if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
13762
14147
  if (!operators.has(binaryGuard.operator)) return false;
13763
- const leftSide = unwrapChainExpression$2(binaryGuard.left);
13764
- const rightSide = unwrapChainExpression$2(binaryGuard.right);
14148
+ const leftSide = unwrapChainExpression$3(binaryGuard.left);
14149
+ const rightSide = unwrapChainExpression$3(binaryGuard.right);
13765
14150
  if (!isMemberProperty(leftSide, "length")) return false;
13766
14151
  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);
14152
+ const leftLengthObject = unwrapChainExpression$3(leftSide.object);
14153
+ const rightLengthObject = unwrapChainExpression$3(rightSide.object);
14154
+ const normalizedReceiver = unwrapChainExpression$3(receiverArray);
14155
+ const normalizedIndexed = unwrapChainExpression$3(indexedArray);
13771
14156
  const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
13772
14157
  const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
13773
14158
  return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
@@ -13854,18 +14239,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
13854
14239
  "toReversed"
13855
14240
  ]);
13856
14241
  const peelLengthPreservingDerivation = (expression) => {
13857
- let current = unwrapChainExpression$2(expression);
14242
+ let current = unwrapChainExpression$3(expression);
13858
14243
  for (;;) {
13859
14244
  if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
13860
- current = unwrapChainExpression$2(current.elements[0].argument);
14245
+ current = unwrapChainExpression$3(current.elements[0].argument);
13861
14246
  continue;
13862
14247
  }
13863
14248
  if (isNodeOfType(current, "CallExpression")) {
13864
- const callee = unwrapChainExpression$2(current.callee);
14249
+ const callee = unwrapChainExpression$3(current.callee);
13865
14250
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
13866
- const calleeObject = unwrapChainExpression$2(callee.object);
14251
+ const calleeObject = unwrapChainExpression$3(callee.object);
13867
14252
  if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
13868
- current = unwrapChainExpression$2(current.arguments[0]);
14253
+ current = unwrapChainExpression$3(current.arguments[0]);
13869
14254
  continue;
13870
14255
  }
13871
14256
  if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
@@ -13910,7 +14295,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
13910
14295
  const initializer = findConstInitializer(current.name, scopeNode);
13911
14296
  if (!initializer) return current;
13912
14297
  const peeledInitializer = peelLengthPreservingDerivation(initializer);
13913
- if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
14298
+ if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
13914
14299
  current = peeledInitializer;
13915
14300
  }
13916
14301
  return current;
@@ -19640,54 +20025,6 @@ const nextjsNoAElement = defineRule({
19640
20025
  } })
19641
20026
  });
19642
20027
  //#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
20028
  //#region src/plugin/utils/contains-fetch-call.ts
19692
20029
  const isFetchCall$1 = (node) => {
19693
20030
  if (!isNodeOfType(node, "CallExpression")) return false;
@@ -20671,6 +21008,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20671
21008
  continue;
20672
21009
  }
20673
21010
  if (isNodeOfType(statement, "ExportNamedDeclaration")) {
21011
+ if (statement.exportKind === "type") continue;
20674
21012
  const declaration = statement.declaration;
20675
21013
  if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
20676
21014
  recordVariableDeclaration(declaration);
@@ -20685,6 +21023,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
20685
21023
  }
20686
21024
  for (const specifier of statement.specifiers ?? []) {
20687
21025
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21026
+ if (specifier.exportKind === "type") continue;
20688
21027
  const local = specifier.local;
20689
21028
  const exported = specifier.exported;
20690
21029
  if (!isNodeOfType(local, "Identifier")) continue;
@@ -20733,25 +21072,37 @@ const resolveImportedExportName = (importSpecifier) => {
20733
21072
  if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
20734
21073
  return null;
20735
21074
  };
20736
- const findReExportSourcesForName = (programRoot, exportedName) => {
21075
+ const findReExportTargetsForName = (programRoot, exportedName) => {
20737
21076
  if (!isNodeOfType(programRoot, "Program")) return [];
20738
- const exportAllSources = [];
21077
+ const exportAllTargets = [];
20739
21078
  for (const statement of programRoot.body ?? []) {
20740
21079
  if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
21080
+ if (statement.exportKind === "type") continue;
20741
21081
  const sourceValue = statement.source.value;
20742
21082
  if (typeof sourceValue !== "string") continue;
20743
21083
  for (const specifier of statement.specifiers ?? []) {
20744
21084
  if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
21085
+ if (specifier.exportKind === "type") continue;
20745
21086
  const exported = specifier.exported;
20746
- if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
21087
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
21088
+ const local = specifier.local;
21089
+ const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
21090
+ if (importedName) return [{
21091
+ importedName,
21092
+ source: sourceValue
21093
+ }];
20747
21094
  }
20748
21095
  }
20749
21096
  if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
21097
+ if (statement.exportKind === "type" || statement.exported) continue;
20750
21098
  const sourceValue = statement.source.value;
20751
- if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
21099
+ if (typeof sourceValue === "string") exportAllTargets.push({
21100
+ importedName: exportedName,
21101
+ source: sourceValue
21102
+ });
20752
21103
  }
20753
21104
  }
20754
- return exportAllSources;
21105
+ return exportAllTargets;
20755
21106
  };
20756
21107
  //#endregion
20757
21108
  //#region src/plugin/utils/attach-parent-references.ts
@@ -20840,139 +21191,6 @@ const parseSourceFile = (absoluteFilePath) => {
20840
21191
  return parsedProgram;
20841
21192
  };
20842
21193
  //#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
21194
  //#region src/plugin/utils/resolve-relative-import-path.ts
20977
21195
  const MODULE_FILE_EXTENSIONS = [
20978
21196
  ".ts",
@@ -21089,35 +21307,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
21089
21307
  };
21090
21308
  const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
21091
21309
  //#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
21310
  //#region src/plugin/utils/resolve-tsconfig-alias.ts
21122
21311
  const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
21123
21312
  const isObjectRecord = (value) => typeof value === "object" && value !== null;
@@ -21296,25 +21485,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
21296
21485
  };
21297
21486
  //#endregion
21298
21487
  //#region src/plugin/utils/resolve-module-path.ts
21299
- const resolveModulePath = (fromFilename, source) => resolveRelativeImportPath(fromFilename, source) ?? resolveTsconfigAliasPath(fromFilename, source);
21488
+ const resolveModulePath = (fromFilename, source) => {
21489
+ if (path.isAbsolute(source)) return null;
21490
+ return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
21491
+ };
21300
21492
  //#endregion
21301
21493
  //#region src/plugin/utils/resolve-cross-file-function-export.ts
21302
21494
  const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
21303
21495
  if (visitedFilePaths.size >= 4) return null;
21304
21496
  if (visitedFilePaths.has(filePath)) return null;
21305
21497
  visitedFilePaths.add(filePath);
21306
- const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
21307
- const programRoot = parseSourceFile(actualFilePath);
21498
+ const programRoot = parseSourceFile(filePath);
21308
21499
  if (!programRoot) return null;
21309
21500
  const exported = findExportedFunctionBody(programRoot, exportedName);
21310
21501
  if (exported) return exported;
21311
- for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
21312
- const nextFilePath = resolveModulePath(actualFilePath, reExportSource);
21502
+ const resolvedCandidates = /* @__PURE__ */ new Set();
21503
+ for (const target of findReExportTargetsForName(programRoot, exportedName)) {
21504
+ const nextFilePath = resolveModulePath(filePath, target.source);
21313
21505
  if (!nextFilePath) continue;
21314
- const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
21315
- if (resolved) return resolved;
21506
+ const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
21507
+ if (resolved) resolvedCandidates.add(resolved);
21316
21508
  }
21317
- return null;
21509
+ if (resolvedCandidates.size !== 1) return null;
21510
+ return resolvedCandidates.values().next().value ?? null;
21318
21511
  };
21319
21512
  const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
21320
21513
  const resolvedFilePath = resolveModulePath(fromFilename, source);
@@ -22497,6 +22690,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22497
22690
  "parseInt"
22498
22691
  ]);
22499
22692
  const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22693
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22694
+ "isRawJSON",
22695
+ "parse",
22696
+ "rawJSON",
22697
+ "stringify"
22698
+ ])], ["Math", new Set([
22699
+ "abs",
22700
+ "acos",
22701
+ "acosh",
22702
+ "asin",
22703
+ "asinh",
22704
+ "atan",
22705
+ "atan2",
22706
+ "atanh",
22707
+ "cbrt",
22708
+ "ceil",
22709
+ "clz32",
22710
+ "cos",
22711
+ "cosh",
22712
+ "exp",
22713
+ "floor",
22714
+ "fround",
22715
+ "hypot",
22716
+ "imul",
22717
+ "log",
22718
+ "log10",
22719
+ "log1p",
22720
+ "log2",
22721
+ "max",
22722
+ "min",
22723
+ "pow",
22724
+ "round",
22725
+ "sign",
22726
+ "sin",
22727
+ "sinh",
22728
+ "sqrt",
22729
+ "tan",
22730
+ "tanh",
22731
+ "trunc"
22732
+ ])]]);
22500
22733
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22501
22734
  "concat",
22502
22735
  "filter",
@@ -22543,6 +22776,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
22543
22776
  }
22544
22777
  return parameter;
22545
22778
  };
22779
+ const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
22780
+ const isModuleFunction = (functionNode) => {
22781
+ let ancestor = functionNode.parent;
22782
+ while (ancestor) {
22783
+ if (isFunctionLike$1(ancestor)) return false;
22784
+ if (isNodeOfType(ancestor, "Program")) return true;
22785
+ ancestor = ancestor.parent;
22786
+ }
22787
+ return false;
22788
+ };
22789
+ const getFunctionBindingNames = (functionNode) => {
22790
+ const names = /* @__PURE__ */ new Set();
22791
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
22792
+ const parent = functionNode.parent;
22793
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
22794
+ return names;
22795
+ };
22796
+ const collectModuleBindingNames = (functionNode) => {
22797
+ let program = functionNode.parent;
22798
+ while (program && !isNodeOfType(program, "Program")) program = program.parent;
22799
+ const bindingNames = /* @__PURE__ */ new Set();
22800
+ if (!program || !isNodeOfType(program, "Program")) return bindingNames;
22801
+ for (const statement of program.body ?? []) {
22802
+ if (isNodeOfType(statement, "ImportDeclaration")) {
22803
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
22804
+ continue;
22805
+ }
22806
+ const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
22807
+ if (isNodeOfType(declaration, "VariableDeclaration")) {
22808
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
22809
+ continue;
22810
+ }
22811
+ if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
22812
+ }
22813
+ return bindingNames;
22814
+ };
22546
22815
  const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
22547
22816
  const substitutions = /* @__PURE__ */ new Map();
22548
22817
  const parameters = getFunctionParameters(functionNode);
@@ -22673,7 +22942,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
22673
22942
  for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
22674
22943
  return introducedBindings;
22675
22944
  };
22676
- const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22945
+ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
22677
22946
  const effectFunction = getEffectFn(analysis, effectNode);
22678
22947
  if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
22679
22948
  const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
@@ -22682,7 +22951,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22682
22951
  invocation: null,
22683
22952
  isDeferred: false,
22684
22953
  introducedBindings: /* @__PURE__ */ new Set(),
22685
- substitutions: /* @__PURE__ */ new Map()
22954
+ substitutions: /* @__PURE__ */ new Map(),
22955
+ currentFilename
22686
22956
  };
22687
22957
  const frames = [rootFrame];
22688
22958
  walkAst(effectFunction, (child) => {
@@ -22703,7 +22973,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
22703
22973
  invocation: child,
22704
22974
  isDeferred,
22705
22975
  introducedBindings,
22706
- substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
22976
+ substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
22977
+ currentFilename
22707
22978
  });
22708
22979
  };
22709
22980
  if (isFunctionLike$1(callee)) {
@@ -22756,6 +23027,174 @@ const isOpaqueHookCall = (callExpression) => {
22756
23027
  const calleeName = getCallCalleeName$1(callExpression);
22757
23028
  return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
22758
23029
  };
23030
+ const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
23031
+ let canContinue = true;
23032
+ for (const statement of statements) {
23033
+ if (!canContinue) {
23034
+ if (!isNodeOfType(statement, "EmptyStatement")) return {
23035
+ canContinue: false,
23036
+ isValid: false
23037
+ };
23038
+ continue;
23039
+ }
23040
+ if (isNodeOfType(statement, "EmptyStatement")) continue;
23041
+ if (isNodeOfType(statement, "ReturnStatement")) {
23042
+ if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
23043
+ canContinue: false,
23044
+ isValid: false
23045
+ };
23046
+ canContinue = false;
23047
+ continue;
23048
+ }
23049
+ if (isNodeOfType(statement, "BlockStatement")) {
23050
+ const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
23051
+ if (!blockSummary.isValid) return blockSummary;
23052
+ canContinue = blockSummary.canContinue;
23053
+ continue;
23054
+ }
23055
+ if (isNodeOfType(statement, "IfStatement")) {
23056
+ if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
23057
+ canContinue: false,
23058
+ isValid: false
23059
+ };
23060
+ const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
23061
+ if (!consequentSummary.isValid) return consequentSummary;
23062
+ const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
23063
+ canContinue: true,
23064
+ isValid: true
23065
+ };
23066
+ if (!alternateSummary.isValid) return alternateSummary;
23067
+ canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
23068
+ continue;
23069
+ }
23070
+ return {
23071
+ canContinue: false,
23072
+ isValid: false
23073
+ };
23074
+ }
23075
+ return {
23076
+ canContinue,
23077
+ isValid: true
23078
+ };
23079
+ };
23080
+ const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
23081
+ const node = stripParenExpression(expression);
23082
+ if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
23083
+ if (isNodeOfType(node, "Identifier")) {
23084
+ const parameterIndex = environment.parameterIndices.get(node.name);
23085
+ if (parameterIndex !== void 0) {
23086
+ if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
23087
+ return true;
23088
+ }
23089
+ return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
23090
+ }
23091
+ if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
23092
+ if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
23093
+ if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
23094
+ if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
23095
+ if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
23096
+ return analyzeHelperExpression(property.value, environment, usedParameterIndices);
23097
+ });
23098
+ if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
23099
+ if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23100
+ if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
23101
+ if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
23102
+ if (isNodeOfType(node, "MemberExpression")) {
23103
+ if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
23104
+ return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
23105
+ }
23106
+ if (isFunctionLike$1(node)) {
23107
+ if (isAsyncOrGeneratorFunction(node)) return false;
23108
+ const callbackParameterIndices = new Map(environment.parameterIndices);
23109
+ for (const parameter of getFunctionParameters(node)) {
23110
+ if (!isNodeOfType(parameter, "Identifier")) return false;
23111
+ callbackParameterIndices.set(parameter.name, null);
23112
+ }
23113
+ const callbackEnvironment = {
23114
+ parameterIndices: callbackParameterIndices,
23115
+ recursiveNames: environment.recursiveNames,
23116
+ shadowedGlobalNames: environment.shadowedGlobalNames
23117
+ };
23118
+ if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
23119
+ const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23120
+ return callbackSummary.isValid && !callbackSummary.canContinue;
23121
+ }
23122
+ if (isNodeOfType(node, "CallExpression")) {
23123
+ const callee = stripParenExpression(node.callee);
23124
+ const calleeRoot = getMemberRoot(callee);
23125
+ 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);
23126
+ const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23127
+ const namespaceMemberName = getStaticMemberName(callee);
23128
+ const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23129
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23130
+ if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23131
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23132
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23133
+ }
23134
+ if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
23135
+ return false;
23136
+ };
23137
+ const helperSummaryCache = /* @__PURE__ */ new WeakMap();
23138
+ const summarizeHelperReturn = (functionNode) => {
23139
+ if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
23140
+ if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
23141
+ if (isAsyncOrGeneratorFunction(functionNode)) {
23142
+ helperSummaryCache.set(functionNode, null);
23143
+ return null;
23144
+ }
23145
+ const parameterIndices = /* @__PURE__ */ new Map();
23146
+ const parameters = getFunctionParameters(functionNode);
23147
+ for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
23148
+ const parameter = parameters[parameterIndex];
23149
+ if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
23150
+ helperSummaryCache.set(functionNode, null);
23151
+ return null;
23152
+ }
23153
+ parameterIndices.set(parameter.name, parameterIndex);
23154
+ }
23155
+ const environment = {
23156
+ parameterIndices,
23157
+ recursiveNames: getFunctionBindingNames(functionNode),
23158
+ shadowedGlobalNames: collectModuleBindingNames(functionNode)
23159
+ };
23160
+ const usedParameterIndices = /* @__PURE__ */ new Set();
23161
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) {
23162
+ if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
23163
+ helperSummaryCache.set(functionNode, null);
23164
+ return null;
23165
+ }
23166
+ } else {
23167
+ const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
23168
+ if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
23169
+ helperSummaryCache.set(functionNode, null);
23170
+ return null;
23171
+ }
23172
+ }
23173
+ const summary = { usedParameterIndices };
23174
+ helperSummaryCache.set(functionNode, summary);
23175
+ return summary;
23176
+ };
23177
+ const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
23178
+ if (!isNodeOfType(callee, "Identifier")) return null;
23179
+ const reference = getRef(analysis, callee);
23180
+ if (!reference?.resolved) return null;
23181
+ const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
23182
+ if (importDefinition) {
23183
+ if (!currentFilename) return null;
23184
+ const specifier = importDefinition.node;
23185
+ if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
23186
+ const importDeclaration = specifier.parent;
23187
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
23188
+ if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
23189
+ const source = importDeclaration.source?.value;
23190
+ if (typeof source !== "string") return null;
23191
+ const exportedName = resolveImportedExportName(specifier);
23192
+ if (!exportedName) return null;
23193
+ return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
23194
+ }
23195
+ const callable = resolveWrappedCallable(analysis, callee);
23196
+ return callable && isModuleFunction(callable) ? callable : null;
23197
+ };
22759
23198
  const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
22760
23199
  const definitionNode = definition.node;
22761
23200
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
@@ -22841,38 +23280,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
22841
23280
  const calleeRoot = getMemberRoot(callee);
22842
23281
  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
23282
  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;
23283
+ if (isPureGlobalCall || isPureMemberTransform) {
23284
+ if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23285
+ for (const argument of node.arguments ?? []) {
23286
+ if (isFunctionLike$1(argument)) continue;
23287
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23288
+ }
22852
23289
  return evidence;
22853
23290
  }
22854
23291
  if (remainingCallFrames <= 0) {
22855
23292
  evidence.hasUnknownSource = true;
22856
23293
  return evidence;
22857
23294
  }
22858
- const callable = resolveWrappedCallable(analysis, callee);
22859
- if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
22860
- evidence.hasUnknownSource = true;
23295
+ const localHelperFunction = resolveWrappedCallable(analysis, callee);
23296
+ if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
23297
+ if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
23298
+ evidence.hasUnknownSource = true;
23299
+ return evidence;
23300
+ }
23301
+ const localHelperFrame = {
23302
+ functionNode: localHelperFunction,
23303
+ invocation: node,
23304
+ isDeferred: false,
23305
+ introducedBindings: /* @__PURE__ */ new Set(),
23306
+ substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
23307
+ currentFilename: frame.currentFilename
23308
+ };
23309
+ const returnedExpressions = getReturnedExpressions(localHelperFunction);
23310
+ if (returnedExpressions.length === 0) {
23311
+ evidence.hasUnknownSource = true;
23312
+ return evidence;
23313
+ }
23314
+ for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
22861
23315
  return evidence;
22862
23316
  }
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) {
23317
+ const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
23318
+ const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
23319
+ if (!helperSummary) {
22872
23320
  evidence.hasUnknownSource = true;
22873
23321
  return evidence;
22874
23322
  }
22875
- for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
23323
+ const argumentsForHelper = node.arguments ?? [];
23324
+ for (const parameterIndex of helperSummary.usedParameterIndices) {
23325
+ const argument = argumentsForHelper[parameterIndex];
23326
+ if (!argument) {
23327
+ evidence.hasUnknownSource = true;
23328
+ return evidence;
23329
+ }
23330
+ mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
23331
+ }
22876
23332
  return evidence;
22877
23333
  }
22878
23334
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
@@ -22952,8 +23408,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
22952
23408
  }
22953
23409
  return false;
22954
23410
  };
22955
- const collectEffectStateWriteFacts = (analysis, effectNode) => {
22956
- const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
23411
+ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
23412
+ const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
22957
23413
  if (frames.length === 0) return [];
22958
23414
  const effectHasCleanup = hasCleanup(analysis, effectNode);
22959
23415
  const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
@@ -22973,7 +23429,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
22973
23429
  invocation: callExpression,
22974
23430
  isDeferred: frame.isDeferred,
22975
23431
  introducedBindings: collectIntroducedBindings(analysis, writtenValue),
22976
- substitutions: /* @__PURE__ */ new Map()
23432
+ substitutions: /* @__PURE__ */ new Map(),
23433
+ currentFilename
22977
23434
  };
22978
23435
  valueEvidence = emptyEvidence();
22979
23436
  const returnedExpressions = getReturnedExpressions(writtenValue);
@@ -23022,7 +23479,7 @@ const noAdjustStateOnPropChange = defineRule({
23022
23479
  const dependencyReferences = getEffectDepsRefs(analysis, node);
23023
23480
  if (!dependencyReferences) return;
23024
23481
  if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
23025
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
23482
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
23026
23483
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
23027
23484
  context.report({
23028
23485
  node: fact.callExpression,
@@ -24283,6 +24740,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
24283
24740
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
24284
24741
  };
24285
24742
  //#endregion
24743
+ //#region src/plugin/utils/is-barrel-index-module.ts
24744
+ const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
24745
+ const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24746
+ const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24747
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
24748
+ const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
24749
+ const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
24750
+ const createNonBarrelInfo = () => ({
24751
+ isBarrel: false,
24752
+ exportsByName: /* @__PURE__ */ new Map(),
24753
+ starExportSources: []
24754
+ });
24755
+ const addImportedBinding = (importedBindings, binding) => {
24756
+ importedBindings.set(binding.localName, {
24757
+ ...binding,
24758
+ didExport: false
24759
+ });
24760
+ };
24761
+ const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
24762
+ for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
24763
+ localName: specifier.exportedName,
24764
+ importedName: specifier.localName,
24765
+ source,
24766
+ isTypeOnly: specifier.isTypeOnly
24767
+ });
24768
+ };
24769
+ const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
24770
+ const trimmedImportClause = importClause.trim();
24771
+ const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
24772
+ if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
24773
+ localName: namespaceMatch[1],
24774
+ importedName: "*",
24775
+ source,
24776
+ isTypeOnly: declarationIsTypeOnly
24777
+ });
24778
+ const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
24779
+ if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
24780
+ const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
24781
+ if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
24782
+ localName: defaultImportName,
24783
+ importedName: "default",
24784
+ source,
24785
+ isTypeOnly: declarationIsTypeOnly
24786
+ });
24787
+ };
24788
+ const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
24789
+ let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
24790
+ collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
24791
+ return "";
24792
+ });
24793
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
24794
+ const isTypeOnly = Boolean(typeKeyword);
24795
+ if (namespaceExportName) {
24796
+ exportsByName.set(namespaceExportName, {
24797
+ exportedName: namespaceExportName,
24798
+ importedName: "*",
24799
+ source,
24800
+ isTypeOnly
24801
+ });
24802
+ return "";
24803
+ }
24804
+ if (specifiersText) {
24805
+ for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
24806
+ exportedName: specifier.exportedName,
24807
+ importedName: specifier.localName,
24808
+ source,
24809
+ isTypeOnly: specifier.isTypeOnly
24810
+ });
24811
+ return "";
24812
+ }
24813
+ starExportSources.push(source);
24814
+ return "";
24815
+ });
24816
+ withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
24817
+ for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
24818
+ const importedBinding = importedBindings.get(specifier.localName);
24819
+ if (!importedBinding) return _match;
24820
+ importedBinding.didExport = true;
24821
+ exportsByName.set(specifier.exportedName, {
24822
+ exportedName: specifier.exportedName,
24823
+ importedName: importedBinding.importedName,
24824
+ source: importedBinding.source,
24825
+ isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
24826
+ });
24827
+ }
24828
+ return "";
24829
+ });
24830
+ return withoutKnownDeclarations;
24831
+ };
24832
+ const hasUnexportedRuntimeImport = (importedBindings) => {
24833
+ for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
24834
+ return false;
24835
+ };
24836
+ const classifyBarrelModule = (sourceText) => {
24837
+ const strippedSource = stripJsComments(sourceText).trim();
24838
+ if (!strippedSource) return createNonBarrelInfo();
24839
+ const importedBindings = /* @__PURE__ */ new Map();
24840
+ const exportsByName = /* @__PURE__ */ new Map();
24841
+ const starExportSources = [];
24842
+ if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
24843
+ return {
24844
+ isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
24845
+ exportsByName,
24846
+ starExportSources
24847
+ };
24848
+ };
24849
+ const getBarrelIndexModuleInfo = (filePath) => {
24850
+ if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
24851
+ recordContentProbe(filePath);
24852
+ let fileStat;
24853
+ try {
24854
+ fileStat = fs.statSync(filePath);
24855
+ } catch {
24856
+ fileStat = null;
24857
+ }
24858
+ const cachedResult = barrelIndexModuleInfoCache.get(filePath);
24859
+ if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
24860
+ if (fileStat === null) return createNonBarrelInfo();
24861
+ let moduleInfo = createNonBarrelInfo();
24862
+ try {
24863
+ moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
24864
+ } catch {
24865
+ moduleInfo = createNonBarrelInfo();
24866
+ }
24867
+ barrelIndexModuleInfoCache.set(filePath, {
24868
+ mtimeMs: fileStat.mtimeMs,
24869
+ size: fileStat.size,
24870
+ moduleInfo
24871
+ });
24872
+ return moduleInfo;
24873
+ };
24874
+ const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
24875
+ //#endregion
24286
24876
  //#region src/react-native-dependency-names.ts
24287
24877
  const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
24288
24878
  "expo",
@@ -24485,6 +25075,35 @@ const classifyReactNativeFileTarget = (context) => {
24485
25075
  };
24486
25076
  const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
24487
25077
  //#endregion
25078
+ //#region src/plugin/utils/resolve-barrel-export-file-path.ts
25079
+ const getUniqueFilePath = (filePaths) => {
25080
+ const uniqueFilePaths = new Set(filePaths);
25081
+ if (uniqueFilePaths.size !== 1) return null;
25082
+ const [filePath] = uniqueFilePaths;
25083
+ return filePath ?? null;
25084
+ };
25085
+ const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
25086
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
25087
+ if (!resolvedTargetPath) return null;
25088
+ const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
25089
+ if (nestedTargetPath) return nestedTargetPath;
25090
+ return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
25091
+ };
25092
+ const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
25093
+ if (visitedFilePaths.has(barrelFilePath)) return null;
25094
+ visitedFilePaths.add(barrelFilePath);
25095
+ const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
25096
+ if (!moduleInfo.isBarrel) return null;
25097
+ const target = moduleInfo.exportsByName.get(exportedName);
25098
+ if (target) {
25099
+ const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
25100
+ if (!resolvedTargetPath) return null;
25101
+ return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
25102
+ }
25103
+ if (exportedName === "default") return null;
25104
+ return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
25105
+ };
25106
+ //#endregion
24488
25107
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
24489
25108
  const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
24490
25109
  const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
@@ -24814,7 +25433,7 @@ const isAsyncFunctionLike = (node) => {
24814
25433
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
24815
25434
  return false;
24816
25435
  };
24817
- const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
25436
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
24818
25437
  "forEach",
24819
25438
  "map",
24820
25439
  "filter",
@@ -24835,7 +25454,7 @@ const runsOnEffectDispatch = (functionNode) => {
24835
25454
  if (parent.callee === functionNode) return true;
24836
25455
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
24837
25456
  const callee = parent.callee;
24838
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
25457
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
24839
25458
  };
24840
25459
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
24841
25460
  const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
@@ -25800,7 +26419,7 @@ const noDerivedState = defineRule({
25800
26419
  if (!isUseEffect(node)) return;
25801
26420
  const analysis = getProgramAnalysis(node);
25802
26421
  if (!analysis) return;
25803
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
26422
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
25804
26423
  if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
25805
26424
  const stateName = getStateName$1(fact.stateDeclarator);
25806
26425
  context.report({
@@ -25822,7 +26441,7 @@ const noDerivedStateEffect = defineRule({
25822
26441
  if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25823
26442
  const analysis = getProgramAnalysis(node);
25824
26443
  if (!analysis) return;
25825
- if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26444
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
25826
26445
  context.report({
25827
26446
  node,
25828
26447
  message: "You pay an extra render for state you can derive from other values."
@@ -27468,14 +28087,14 @@ const hasDocumentClassListMutation = (node) => {
27468
28087
  //#endregion
27469
28088
  //#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
27470
28089
  const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
27471
- const unwrapChainExpression$1 = (node) => {
28090
+ const unwrapChainExpression$2 = (node) => {
27472
28091
  if (!node) return null;
27473
28092
  if (isNodeOfType(node, "ChainExpression")) return node.expression;
27474
28093
  return node;
27475
28094
  };
27476
28095
  const collectGuardExpressions = (node, into) => {
27477
28096
  if (!node) return;
27478
- const unwrappedNode = unwrapChainExpression$1(node);
28097
+ const unwrappedNode = unwrapChainExpression$2(node);
27479
28098
  if (!unwrappedNode) return;
27480
28099
  const rootIdentifierName = getRootIdentifierName(unwrappedNode);
27481
28100
  if (rootIdentifierName) {
@@ -27506,7 +28125,7 @@ const isReturnOnlyStatement = (node) => {
27506
28125
  };
27507
28126
  const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
27508
28127
  const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
27509
- const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
28128
+ const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
27510
28129
  if (!unwrappedDependencyExpression) return false;
27511
28130
  if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
27512
28131
  return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
@@ -29565,6 +30184,405 @@ const noGrayOnColoredBackground = defineRule({
29565
30184
  } })
29566
30185
  });
29567
30186
  //#endregion
30187
+ //#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
30188
+ const findEnclosingJsxOpeningElement = (node) => {
30189
+ let cursor = node.parent;
30190
+ while (cursor) {
30191
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30192
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30193
+ cursor = cursor.parent ?? null;
30194
+ }
30195
+ return null;
30196
+ };
30197
+ //#endregion
30198
+ //#region src/plugin/utils/has-client-render-evidence.ts
30199
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30200
+ if (fileHasUseClientDirective) return true;
30201
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30202
+ if (displayName && isReactHookName(displayName)) return true;
30203
+ let callsHook = false;
30204
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30205
+ if (callsHook) return false;
30206
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30207
+ callsHook = true;
30208
+ return false;
30209
+ }
30210
+ });
30211
+ return callsHook;
30212
+ };
30213
+ //#endregion
30214
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
30215
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
30216
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
30217
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
30218
+ return false;
30219
+ };
30220
+ //#endregion
30221
+ //#region src/plugin/utils/read-initial-state-boolean.ts
30222
+ const readLogicalResult = (operator, leftResult, rightResult) => {
30223
+ if (operator === "&&") {
30224
+ if (leftResult === false || rightResult === false) return false;
30225
+ if (leftResult === true && rightResult === true) return true;
30226
+ return null;
30227
+ }
30228
+ if (leftResult === true || rightResult === true) return true;
30229
+ if (leftResult === false && rightResult === false) return false;
30230
+ return null;
30231
+ };
30232
+ const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
30233
+ if (!isNodeOfType(identifier, "Identifier")) return null;
30234
+ if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
30235
+ const binding = findVariableInitializer(identifier, identifier.name);
30236
+ if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
30237
+ visitedBindings.add(binding.bindingIdentifier);
30238
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
30239
+ if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
30240
+ const initializer = stripParenExpression(declarator.init);
30241
+ if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
30242
+ const initialState = initializer.arguments?.[0];
30243
+ if (!initialState) return false;
30244
+ if (initialState.type === "SpreadElement") return null;
30245
+ return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
30246
+ }
30247
+ const declaration = declarator.parent;
30248
+ if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
30249
+ return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
30250
+ };
30251
+ const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
30252
+ const unwrappedExpression = stripParenExpression(expression);
30253
+ if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
30254
+ if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
30255
+ if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
30256
+ if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
30257
+ if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
30258
+ if (unwrappedExpression.body.body.length !== 1) return null;
30259
+ const returnStatement = unwrappedExpression.body.body[0];
30260
+ if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
30261
+ return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
30262
+ }
30263
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
30264
+ const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
30265
+ return argumentResult === null ? null : !argumentResult;
30266
+ }
30267
+ 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));
30268
+ return null;
30269
+ };
30270
+ const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
30271
+ //#endregion
30272
+ //#region src/plugin/utils/is-after-client-only-early-return.ts
30273
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
30274
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30275
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30276
+ const ancestors = /* @__PURE__ */ new Set();
30277
+ let currentNode = node;
30278
+ while (currentNode) {
30279
+ ancestors.add(currentNode);
30280
+ currentNode = currentNode.parent ?? null;
30281
+ }
30282
+ for (const statement of body.body ?? []) {
30283
+ if (ancestors.has(statement)) return false;
30284
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30285
+ const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
30286
+ if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
30287
+ if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
30288
+ }
30289
+ return false;
30290
+ };
30291
+ //#endregion
30292
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
30293
+ const isGatedByFalsyInitialState = (node, scopes) => {
30294
+ let cursor = node;
30295
+ let parent = node.parent;
30296
+ while (parent) {
30297
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
30298
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
30299
+ if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
30300
+ cursor = parent;
30301
+ parent = parent.parent ?? null;
30302
+ }
30303
+ return false;
30304
+ };
30305
+ //#endregion
30306
+ //#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
30307
+ const evaluateEquality = (operator, left, right) => {
30308
+ if (operator === "===" || operator === "==") return left === right;
30309
+ if (operator === "!==" || operator === "!=") return left !== right;
30310
+ return null;
30311
+ };
30312
+ const readTypeofBrowserGlobal = (expression, context) => {
30313
+ const unwrappedExpression = stripParenExpression(expression);
30314
+ if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
30315
+ const argument = stripParenExpression(unwrappedExpression.argument);
30316
+ if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
30317
+ 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;
30318
+ return argument.property.name;
30319
+ };
30320
+ const matchBrowserPredicate = (expression, context) => {
30321
+ const unwrappedExpression = stripParenExpression(expression);
30322
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
30323
+ const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
30324
+ return innerMatch ? {
30325
+ browserGlobalName: innerMatch.browserGlobalName,
30326
+ clientResult: !innerMatch.clientResult,
30327
+ serverResult: !innerMatch.serverResult
30328
+ } : null;
30329
+ }
30330
+ if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
30331
+ const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
30332
+ const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
30333
+ const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
30334
+ const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
30335
+ const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
30336
+ const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
30337
+ if (!browserGlobalName || !comparedType) return null;
30338
+ const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
30339
+ const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
30340
+ if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
30341
+ return {
30342
+ browserGlobalName,
30343
+ clientResult,
30344
+ serverResult
30345
+ };
30346
+ };
30347
+ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
30348
+ if (operator === "&&") {
30349
+ if (leftResult === false || rightResult === false) return false;
30350
+ if (leftResult === true && rightResult === true) return true;
30351
+ return null;
30352
+ }
30353
+ if (leftResult === true || rightResult === true) return true;
30354
+ if (leftResult === false && rightResult === false) return false;
30355
+ return null;
30356
+ };
30357
+ const readHydrationConditionResult = (expression, context, runtime) => {
30358
+ const unwrappedExpression = stripParenExpression(expression);
30359
+ const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
30360
+ if (predicateMatch) return predicateMatch[`${runtime}Result`];
30361
+ const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
30362
+ if (staticResult !== null) return staticResult;
30363
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
30364
+ const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
30365
+ return argumentResult === null ? null : !argumentResult;
30366
+ }
30367
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
30368
+ return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
30369
+ };
30370
+ const matchHydrationCondition = (expression, context) => {
30371
+ const unwrappedExpression = stripParenExpression(expression);
30372
+ const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
30373
+ if (predicateMatch) return {
30374
+ predicateMatch,
30375
+ predicateNode: unwrappedExpression
30376
+ };
30377
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
30378
+ if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
30379
+ const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
30380
+ const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
30381
+ if (leftMatch && rightMatch) {
30382
+ const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
30383
+ const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
30384
+ return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
30385
+ }
30386
+ const nestedMatch = leftMatch ?? rightMatch;
30387
+ if (!nestedMatch) return null;
30388
+ const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
30389
+ if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
30390
+ return nestedMatch;
30391
+ };
30392
+ const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
30393
+ const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
30394
+ if (!leftNode || !rightNode) return leftNode === rightNode;
30395
+ const left = stripParenExpression(leftNode);
30396
+ const right = stripParenExpression(rightNode);
30397
+ if (areExpressionsStructurallyEqual(left, right)) return true;
30398
+ if (left.type !== right.type) return false;
30399
+ if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
30400
+ if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
30401
+ if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
30402
+ return areRenderedBranchesEquivalent(left.expression, right.expression);
30403
+ }
30404
+ if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
30405
+ if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
30406
+ if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
30407
+ return areNodeArraysEquivalent(left.children, right.children);
30408
+ }
30409
+ if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
30410
+ if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
30411
+ if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
30412
+ return areRenderedBranchesEquivalent(left.value, right.value);
30413
+ }
30414
+ if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
30415
+ if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
30416
+ if (left.quasis.length !== right.quasis.length) return false;
30417
+ if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
30418
+ return areNodeArraysEquivalent(left.expressions, right.expressions);
30419
+ }
30420
+ return false;
30421
+ };
30422
+ const isRenderedValue = (node) => {
30423
+ const unwrappedNode = stripParenExpression(node);
30424
+ if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
30425
+ if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
30426
+ return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
30427
+ };
30428
+ const findRenderedValueInAndBranch = (node) => {
30429
+ const unwrappedNode = stripParenExpression(node);
30430
+ if (isRenderedValue(unwrappedNode)) return unwrappedNode;
30431
+ if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
30432
+ return findRenderedValueInAndBranch(unwrappedNode.right);
30433
+ };
30434
+ const findEnclosingJsxAttribute = (node) => {
30435
+ let currentNode = node.parent;
30436
+ while (currentNode) {
30437
+ if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
30438
+ if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
30439
+ currentNode = currentNode.parent;
30440
+ }
30441
+ return null;
30442
+ };
30443
+ const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
30444
+ let currentNode = node;
30445
+ let parentNode = currentNode.parent;
30446
+ while (parentNode) {
30447
+ if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
30448
+ const attribute = findEnclosingJsxAttribute(parentNode);
30449
+ return attribute ? !isEventHandlerAttribute(attribute) : true;
30450
+ }
30451
+ if (isNodeOfType(parentNode, "ReturnStatement")) {
30452
+ if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
30453
+ }
30454
+ if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
30455
+ if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
30456
+ currentNode = parentNode;
30457
+ parentNode = currentNode.parent;
30458
+ }
30459
+ return false;
30460
+ };
30461
+ const getReturnedValues = (statement) => {
30462
+ if (!statement) return [];
30463
+ if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
30464
+ if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
30465
+ if (!isNodeOfType(statement, "BlockStatement")) return [];
30466
+ const returnedValues = [];
30467
+ for (const childStatement of statement.body) {
30468
+ returnedValues.push(...getReturnedValues(childStatement));
30469
+ if (statementAlwaysExits(childStatement)) break;
30470
+ }
30471
+ return returnedValues;
30472
+ };
30473
+ const findFollowingReturnedValues = (ifStatement) => {
30474
+ const parentNode = ifStatement.parent;
30475
+ if (!isNodeOfType(parentNode, "BlockStatement")) return [];
30476
+ const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
30477
+ if (statementIndex < 0) return [];
30478
+ const returnedValues = [];
30479
+ for (const statement of parentNode.body.slice(statementIndex + 1)) {
30480
+ returnedValues.push(...getReturnedValues(statement));
30481
+ if (statementAlwaysExits(statement)) break;
30482
+ }
30483
+ return returnedValues;
30484
+ };
30485
+ const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
30486
+ const left = stripParenExpression(leftExpression);
30487
+ const right = stripParenExpression(rightExpression);
30488
+ if (areExpressionsStructurallyEqual(left, right)) return true;
30489
+ if (left.type !== right.type) return false;
30490
+ if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
30491
+ if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
30492
+ if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
30493
+ return false;
30494
+ };
30495
+ const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
30496
+ if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
30497
+ if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
30498
+ if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
30499
+ if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
30500
+ const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
30501
+ const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
30502
+ return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
30503
+ };
30504
+ const isStructuralRenderedValue = (node) => {
30505
+ if (!node) return false;
30506
+ const unwrappedNode = stripParenExpression(node);
30507
+ return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
30508
+ };
30509
+ const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
30510
+ if (!rightBranch) return false;
30511
+ const left = stripParenExpression(leftBranch);
30512
+ const right = stripParenExpression(rightBranch);
30513
+ return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
30514
+ };
30515
+ const noHydrationBranchOnBrowserGlobal = defineRule({
30516
+ id: "no-hydration-branch-on-browser-global",
30517
+ title: "Server and client render different branches",
30518
+ severity: "error",
30519
+ category: "Correctness",
30520
+ requires: ["ssr"],
30521
+ recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
30522
+ create: (context) => {
30523
+ if (isTestlikeFilename(context.filename)) return {};
30524
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30525
+ let fileHasUseClientDirective = false;
30526
+ let fileIsEmailTemplate = false;
30527
+ const reportedNodes = /* @__PURE__ */ new Set();
30528
+ const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
30529
+ const conditionMatch = matchHydrationCondition(conditionNode, context);
30530
+ if (!conditionMatch) return;
30531
+ const { predicateMatch, predicateNode } = conditionMatch;
30532
+ if (reportedNodes.has(predicateNode)) return;
30533
+ if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
30534
+ const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
30535
+ if (!componentOrHookNode) return;
30536
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30537
+ if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
30538
+ if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
30539
+ const attribute = findEnclosingJsxAttribute(predicateNode);
30540
+ if (!attribute || isEventHandlerAttribute(attribute)) return;
30541
+ }
30542
+ if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
30543
+ if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
30544
+ const openingElement = findEnclosingJsxOpeningElement(predicateNode);
30545
+ if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
30546
+ if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
30547
+ if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
30548
+ reportedNodes.add(predicateNode);
30549
+ context.report({
30550
+ node: predicateNode,
30551
+ message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
30552
+ });
30553
+ };
30554
+ return {
30555
+ Program(node) {
30556
+ fileHasUseClientDirective = hasDirective(node, "use client");
30557
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30558
+ },
30559
+ ConditionalExpression(node) {
30560
+ reportHydrationBranch(node.test, node.consequent, node.alternate, true);
30561
+ },
30562
+ LogicalExpression(node) {
30563
+ if (node.operator !== "&&" && node.operator !== "||") return;
30564
+ const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
30565
+ if (!renderedValue) return;
30566
+ reportHydrationBranch(node, renderedValue, null, true);
30567
+ },
30568
+ IfStatement(node) {
30569
+ if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
30570
+ const consequentValues = getReturnedValues(node.consequent);
30571
+ const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
30572
+ if (consequentValues.length === 0 || alternateValues.length === 0) return;
30573
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
30574
+ if (!componentOrHookNode) return;
30575
+ const enclosingFunction = findEnclosingFunction(node);
30576
+ if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
30577
+ for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
30578
+ if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
30579
+ reportHydrationBranch(node.test, consequentValue, alternateValue, false);
30580
+ }
30581
+ }
30582
+ };
30583
+ }
30584
+ });
30585
+ //#endregion
29568
30586
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
29569
30587
  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
30588
  const noImgLazyWithHighFetchpriority = defineRule({
@@ -29585,6 +30603,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
29585
30603
  } })
29586
30604
  });
29587
30605
  //#endregion
30606
+ //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
30607
+ const TIMER_FUNCTION_NAMES = new Set([
30608
+ "cancelAnimationFrame",
30609
+ "clearInterval",
30610
+ "clearTimeout",
30611
+ "queueMicrotask",
30612
+ "requestAnimationFrame",
30613
+ "setInterval",
30614
+ "setTimeout"
30615
+ ]);
30616
+ const STORAGE_MUTATION_METHOD_NAMES = new Set([
30617
+ "clear",
30618
+ "removeItem",
30619
+ "setItem"
30620
+ ]);
30621
+ const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
30622
+ const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
30623
+ const NOTIFICATION_RECEIVER_NAMES = new Set([
30624
+ "message",
30625
+ "notification",
30626
+ "toast"
30627
+ ]);
30628
+ const NOTIFICATION_METHOD_NAMES = new Set([
30629
+ "error",
30630
+ "info",
30631
+ "loading",
30632
+ "open",
30633
+ "show",
30634
+ "success",
30635
+ "warning"
30636
+ ]);
30637
+ const getMemberCall = (node) => {
30638
+ if (!isNodeOfType(node, "CallExpression")) return null;
30639
+ if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
30640
+ if (!isNodeOfType(node.callee.property, "Identifier")) return null;
30641
+ return {
30642
+ methodName: node.callee.property.name,
30643
+ receiver: stripParenExpression(node.callee.object)
30644
+ };
30645
+ };
30646
+ const isNotificationReceiver = (receiver, scopes) => {
30647
+ if (!isNodeOfType(receiver, "Identifier")) return false;
30648
+ if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
30649
+ const symbol = scopes.symbolFor(receiver);
30650
+ if (symbol?.kind === "import") return true;
30651
+ if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
30652
+ const callee = symbol.initializer.callee;
30653
+ return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
30654
+ };
30655
+ const getKnownImpureCall = (callExpression, scopes) => {
30656
+ if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
30657
+ const memberCall = getMemberCall(callExpression);
30658
+ if (!memberCall) return null;
30659
+ const { methodName, receiver } = memberCall;
30660
+ if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
30661
+ if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
30662
+ if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
30663
+ return null;
30664
+ };
30665
+ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
30666
+ let rootIdentifier = null;
30667
+ if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
30668
+ else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
30669
+ if (!rootIdentifier) return null;
30670
+ const updaterScope = scopes.ownScopeFor(updater);
30671
+ if (!updaterScope) return null;
30672
+ const symbol = scopes.symbolFor(rootIdentifier);
30673
+ if (!symbol) return `the external value "${rootIdentifier.name}"`;
30674
+ if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
30675
+ return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
30676
+ };
30677
+ const findImpureUpdaterOperation = (updater, scopes) => {
30678
+ const analysis = getProgramAnalysis(updater);
30679
+ let operation = null;
30680
+ walkAst(updater, (child) => {
30681
+ if (operation) return false;
30682
+ if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
30683
+ if (isNodeOfType(child, "CallExpression")) {
30684
+ if (isNodeOfType(child.callee, "Identifier") && analysis) {
30685
+ const calleeReference = getRef(analysis, child.callee);
30686
+ if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
30687
+ operation = `the nested state update "${child.callee.name}()"`;
30688
+ return false;
30689
+ }
30690
+ }
30691
+ const impureCall = getKnownImpureCall(child, scopes);
30692
+ if (impureCall) {
30693
+ operation = impureCall;
30694
+ return false;
30695
+ }
30696
+ }
30697
+ if (isNodeOfType(child, "AssignmentExpression")) {
30698
+ operation = getExternalAssignmentDescription(child.left, updater, scopes);
30699
+ if (operation) return false;
30700
+ }
30701
+ if (isNodeOfType(child, "UpdateExpression")) {
30702
+ operation = getExternalAssignmentDescription(child.argument, updater, scopes);
30703
+ if (operation) return false;
30704
+ }
30705
+ });
30706
+ return operation;
30707
+ };
30708
+ const noImpureStateUpdater = defineRule({
30709
+ id: "no-impure-state-updater",
30710
+ title: "State updater has side effects",
30711
+ severity: "error",
30712
+ 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.",
30713
+ create: (context) => ({ CallExpression(node) {
30714
+ const updater = node.arguments?.[0];
30715
+ if (!updater || !isFunctionLike$1(updater)) return;
30716
+ const analysis = getProgramAnalysis(node);
30717
+ if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
30718
+ const calleeReference = getRef(analysis, node.callee);
30719
+ if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
30720
+ const stateDeclarator = getUseStateDecl(analysis, calleeReference);
30721
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
30722
+ const operation = findImpureUpdaterOperation(updater, context.scopes);
30723
+ if (!operation) return;
30724
+ context.report({
30725
+ node: updater,
30726
+ 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.`
30727
+ });
30728
+ } })
30729
+ });
30730
+ //#endregion
29588
30731
  //#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
29589
30732
  const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
29590
30733
  const REACT_USE_REF_OPTIONS = {
@@ -29751,7 +30894,7 @@ const noInitializeState = defineRule({
29751
30894
  if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
29752
30895
  const analysis = getProgramAnalysis(node);
29753
30896
  if (!analysis) return;
29754
- for (const fact of collectEffectStateWriteFacts(analysis, node)) {
30897
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
29755
30898
  if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
29756
30899
  const stateName = getStateName(fact.stateDeclarator);
29757
30900
  context.report({
@@ -30410,111 +31553,6 @@ const noLegacyContextApi = defineRule({
30410
31553
  }
30411
31554
  });
30412
31555
  //#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
31556
  //#region src/plugin/rules/performance/no-locale-format-in-render.ts
30519
31557
  const LOCALE_FORMAT_METHOD_NAMES = new Set([
30520
31558
  "toLocaleString",
@@ -30644,75 +31682,12 @@ const matchDateDefaultStringification = (node) => {
30644
31682
  }
30645
31683
  return null;
30646
31684
  };
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
31685
  const noLocaleFormatInRender = defineRule({
30705
31686
  id: "no-locale-format-in-render",
30706
31687
  title: "Locale/timezone formatting during render",
30707
31688
  severity: "warn",
30708
31689
  category: "Correctness",
30709
- disabledWhen: [
30710
- "vite",
30711
- "cra",
30712
- "expo",
30713
- "react-native",
30714
- "unknown"
30715
- ],
31690
+ requires: ["ssr"],
30716
31691
  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
31692
  create: (context) => {
30718
31693
  if (isTestlikeFilename(context.filename)) return {};
@@ -30722,13 +31697,12 @@ const noLocaleFormatInRender = defineRule({
30722
31697
  const reportedNodes = /* @__PURE__ */ new Set();
30723
31698
  const reportIfRenderPhase = (match) => {
30724
31699
  if (reportedNodes.has(match.node)) return;
30725
- const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
31700
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node, context.scopes);
30726
31701
  if (!componentOrHookNode) return;
30727
31702
  if (fileIsEmailTemplate) return;
30728
31703
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30729
- if (isInsideClientOnlyGuard(match.node)) return;
30730
- if (isGatedByFalsyInitialState(match.node)) return;
30731
- if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
31704
+ if (isGatedByFalsyInitialState(match.node, context.scopes)) return;
31705
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode, context.scopes)) return;
30732
31706
  if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30733
31707
  if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30734
31708
  reportedNodes.add(match.node);
@@ -30755,7 +31729,7 @@ const noLocaleFormatInRender = defineRule({
30755
31729
  if (!isNodeOfType(expression, "CallExpression")) return;
30756
31730
  if (!isNodeOfType(expression.callee, "Identifier")) return;
30757
31731
  const helperName = expression.callee.name;
30758
- const componentOrHookNode = findRenderPhaseComponentOrHook(node);
31732
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
30759
31733
  if (!componentOrHookNode) return;
30760
31734
  const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30761
31735
  if (!helperNode || !isFunctionLike$1(helperNode)) return;
@@ -30767,9 +31741,8 @@ const noLocaleFormatInRender = defineRule({
30767
31741
  if (!match || reportedNodes.has(match.node)) return;
30768
31742
  if (fileIsEmailTemplate) return;
30769
31743
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30770
- if (isInsideClientOnlyGuard(node)) return;
30771
- if (isGatedByFalsyInitialState(node)) return;
30772
- if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
31744
+ if (isGatedByFalsyInitialState(node, context.scopes)) return;
31745
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
30773
31746
  if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30774
31747
  reportedNodes.add(match.node);
30775
31748
  context.report({
@@ -30871,9 +31844,6 @@ const noLongTransitionDuration = defineRule({
30871
31844
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
30872
31845
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
30873
31846
  //#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
31847
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
30878
31848
  const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
30879
31849
  const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
@@ -31005,7 +31975,7 @@ const noMatchMediaInStateInitializer = defineRule({
31005
31975
  title: "matchMedia in state initializer",
31006
31976
  severity: "warn",
31007
31977
  category: "Correctness",
31008
- disabledWhen: ["vite", "cra"],
31978
+ requires: ["ssr"],
31009
31979
  recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
31010
31980
  create: (context) => {
31011
31981
  if (isTestlikeFilename(context.filename)) return {};
@@ -31556,6 +32526,41 @@ const noMutableInDeps = defineRule({
31556
32526
  }
31557
32527
  });
31558
32528
  //#endregion
32529
+ //#region src/plugin/utils/is-result-discarded-call.ts
32530
+ const isResultDiscardedCall = (callExpression) => {
32531
+ let node = callExpression;
32532
+ let parent = node.parent;
32533
+ while (parent) {
32534
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
32535
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
32536
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
32537
+ if (isNodeOfType(parent, "ChainExpression")) {
32538
+ node = parent;
32539
+ parent = node.parent;
32540
+ continue;
32541
+ }
32542
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
32543
+ node = parent;
32544
+ parent = node.parent;
32545
+ continue;
32546
+ }
32547
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
32548
+ node = parent;
32549
+ parent = node.parent;
32550
+ continue;
32551
+ }
32552
+ if (isNodeOfType(parent, "SequenceExpression")) {
32553
+ const expressions = parent.expressions ?? [];
32554
+ if (expressions[expressions.length - 1] !== node) return true;
32555
+ node = parent;
32556
+ parent = node.parent;
32557
+ continue;
32558
+ }
32559
+ return false;
32560
+ }
32561
+ return false;
32562
+ };
32563
+ //#endregion
31559
32564
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
31560
32565
  const LODASH_MUTATOR_NAMES = new Set([
31561
32566
  "set",
@@ -32846,7 +33851,7 @@ const isUseRefIdentifier = (identifier) => {
32846
33851
  };
32847
33852
  const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
32848
33853
  const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
32849
- const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
33854
+ const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
32850
33855
  const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
32851
33856
  "useCallback",
32852
33857
  "useMemo",
@@ -32881,7 +33886,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
32881
33886
  return Boolean(ref.resolved?.defs.some((def) => {
32882
33887
  const node = def.node;
32883
33888
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
32884
- const initializer = unwrapChainExpression(node.init);
33889
+ const initializer = unwrapChainExpression$1(node.init);
32885
33890
  const wrappedFunction = getWrapperHookWrappedFunction(initializer);
32886
33891
  if (wrappedFunction) {
32887
33892
  if (wrappedFunction.async) return false;
@@ -32891,10 +33896,170 @@ const isDirectParentCallbackRef = (analysis, ref) => {
32891
33896
  return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
32892
33897
  }));
32893
33898
  };
33899
+ const getDeclarationKind = (declarator) => {
33900
+ const declaration = declarator.parent;
33901
+ return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
33902
+ };
33903
+ const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
33904
+ const getDestructuredPropertyName = (bindingIdentifier) => {
33905
+ const property = bindingIdentifier.parent;
33906
+ if (!property || !isNodeOfType(property, "Property")) return null;
33907
+ if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
33908
+ return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
33909
+ };
33910
+ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
33911
+ const unwrappedExpression = stripParenExpression(expression);
33912
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
33913
+ const callbackReference = getRef(analysis, unwrappedExpression);
33914
+ const callbackVariable = callbackReference?.resolved;
33915
+ if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
33916
+ if (isProp(analysis, callbackReference)) {
33917
+ if (isWholePropsObjectReference(analysis, callbackReference)) return null;
33918
+ const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
33919
+ return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
33920
+ }
33921
+ if (hasMutableBindingWrite(callbackReference)) return null;
33922
+ const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
33923
+ if (definitions.length !== 1) return null;
33924
+ const declarator = definitions[0];
33925
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
33926
+ visitedVariables.add(callbackVariable);
33927
+ if (isNodeOfType(declarator.id, "ObjectPattern")) {
33928
+ const bindingIdentifier = callbackVariable.defs[0]?.name;
33929
+ const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
33930
+ const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
33931
+ return propertyName && propsReference ? propertyName : null;
33932
+ }
33933
+ if (!isNodeOfType(declarator.id, "Identifier")) return null;
33934
+ return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
33935
+ }
33936
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
33937
+ const callbackName = getStaticMemberPropertyName(unwrappedExpression);
33938
+ if (!callbackName) return null;
33939
+ const receiver = stripParenExpression(unwrappedExpression.object);
33940
+ if (!isNodeOfType(receiver, "Identifier")) return null;
33941
+ const receiverReference = getRef(analysis, receiver);
33942
+ if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
33943
+ return callbackName;
33944
+ };
33945
+ const isNullishRefInitializer = (initializer) => {
33946
+ if (!initializer) return true;
33947
+ const unwrappedInitializer = stripParenExpression(initializer);
33948
+ if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
33949
+ if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
33950
+ return unwrappedInitializer.name === "undefined";
33951
+ };
33952
+ const getDirectComponentBodyStatement = (node, componentBody) => {
33953
+ let current = node;
33954
+ while (current?.parent && current.parent !== componentBody) current = current.parent;
33955
+ return current?.parent === componentBody ? current : null;
33956
+ };
33957
+ const getVariableForDeclarator = (analysis, declarator) => {
33958
+ for (const scope of analysis.scopeManager.scopes) {
33959
+ const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
33960
+ if (variable) return variable;
33961
+ }
33962
+ return null;
33963
+ };
33964
+ const getRefMember = (identifier) => {
33965
+ const receiver = findTransparentExpressionRoot(identifier);
33966
+ const member = receiver.parent;
33967
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
33968
+ return member;
33969
+ };
33970
+ const getRefMemberAssignment = (identifier) => {
33971
+ const member = getRefMember(identifier);
33972
+ if (!member) return null;
33973
+ const memberRoot = findTransparentExpressionRoot(member);
33974
+ const assignment = memberRoot.parent;
33975
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
33976
+ return assignment;
33977
+ };
33978
+ const getRefAliasDeclarator = (identifier) => {
33979
+ const initializer = findTransparentExpressionRoot(identifier);
33980
+ const declarator = initializer.parent;
33981
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
33982
+ return declarator;
33983
+ };
33984
+ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
33985
+ if (!isNodeOfType(receiver, "Identifier")) return null;
33986
+ const receiverReference = getRef(analysis, receiver);
33987
+ if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
33988
+ const variables = /* @__PURE__ */ new Set();
33989
+ let currentVariable = receiverReference.resolved;
33990
+ let refCall = null;
33991
+ while (currentVariable && !variables.has(currentVariable)) {
33992
+ variables.add(currentVariable);
33993
+ const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
33994
+ if (definitions.length !== 1) return null;
33995
+ const declarator = definitions[0];
33996
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
33997
+ if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
33998
+ refCall = declarator.init;
33999
+ break;
34000
+ }
34001
+ if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
34002
+ const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
34003
+ if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
34004
+ currentVariable = upstreamReference.resolved;
34005
+ }
34006
+ if (!refCall) return null;
34007
+ const pendingVariables = [...variables];
34008
+ while (pendingVariables.length > 0) {
34009
+ const variable = pendingVariables.pop();
34010
+ if (!variable) continue;
34011
+ for (const candidateReference of variable.references) {
34012
+ const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
34013
+ if (!aliasDeclarator) continue;
34014
+ const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
34015
+ if (!aliasVariable || variables.has(aliasVariable)) continue;
34016
+ if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
34017
+ variables.add(aliasVariable);
34018
+ pendingVariables.push(aliasVariable);
34019
+ }
34020
+ }
34021
+ return {
34022
+ refCall,
34023
+ variables
34024
+ };
34025
+ };
34026
+ const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
34027
+ const callee = stripParenExpression(callExpression.callee);
34028
+ if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
34029
+ const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
34030
+ if (!bindingProvenance) return null;
34031
+ const { refCall, variables } = bindingProvenance;
34032
+ const componentFunction = findEnclosingFunction(effectCall);
34033
+ if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
34034
+ if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
34035
+ const callbackPropNames = /* @__PURE__ */ new Set();
34036
+ const initializer = refCall.arguments?.[0];
34037
+ const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
34038
+ if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
34039
+ else if (!isNullishRefInitializer(initializer)) return null;
34040
+ for (const variable of variables) for (const candidateReference of variable.references) {
34041
+ if (candidateReference.init) continue;
34042
+ if (candidateReference.isWrite()) return null;
34043
+ const identifier = candidateReference.identifier;
34044
+ if (getRefAliasDeclarator(identifier)) continue;
34045
+ const member = getRefMember(identifier);
34046
+ if (!member || getStaticMemberPropertyName(member) !== "current") return null;
34047
+ const memberParent = findTransparentExpressionRoot(member).parent;
34048
+ if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
34049
+ const assignment = getRefMemberAssignment(identifier);
34050
+ if (!assignment) continue;
34051
+ const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
34052
+ if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
34053
+ const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
34054
+ if (!assignedCallbackName) return null;
34055
+ callbackPropNames.add(assignedCallbackName);
34056
+ }
34057
+ return callbackPropNames.size > 0 ? { callbackPropNames } : null;
34058
+ };
32894
34059
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
32895
34060
  const node = def.node;
32896
34061
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
32897
- return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
34062
+ return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
32898
34063
  }));
32899
34064
  const isHandlerBagArgument = (analysis, argument) => {
32900
34065
  if (!isNodeOfType(argument, "ObjectExpression")) return false;
@@ -32916,7 +34081,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
32916
34081
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
32917
34082
  const node = def.node;
32918
34083
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
32919
- const init = unwrapChainExpression(node.init);
34084
+ const init = unwrapChainExpression$1(node.init);
32920
34085
  if (!isNodeOfType(init, "CallExpression")) return false;
32921
34086
  const callee = init.callee;
32922
34087
  if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
@@ -32946,71 +34111,79 @@ const noPassDataToParent = defineRule({
32946
34111
  severity: "warn",
32947
34112
  tags: ["test-noise"],
32948
34113
  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
- } })
34114
+ create: (context) => {
34115
+ const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
34116
+ allowGlobalReactNamespace: true,
34117
+ allowUnboundBareCalls: true
34118
+ });
34119
+ return { CallExpression(node) {
34120
+ if (!isUseEffect(node)) return;
34121
+ const analysis = getProgramAnalysis(node);
34122
+ if (!analysis) return;
34123
+ if (hasCleanup(analysis, node)) return;
34124
+ const effectFnRefs = getEffectFnRefs(analysis, node);
34125
+ if (!effectFnRefs) return;
34126
+ const effectFn = getEffectFn(analysis, node);
34127
+ if (!effectFn) return;
34128
+ for (const ref of effectFnRefs) {
34129
+ const callExpr = getCallExpr(ref);
34130
+ if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
34131
+ const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
34132
+ if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
34133
+ if (!isSynchronous(ref.identifier, effectFn)) continue;
34134
+ const calleeNode = unwrapChainExpression$1(callExpr.callee);
34135
+ const identifier = ref.identifier;
34136
+ if (callbackRefProvenance) {
34137
+ if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
34138
+ } else if (calleeNode === identifier) {
34139
+ if (!isDirectParentCallbackRef(analysis, ref)) continue;
34140
+ if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
34141
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
34142
+ if (!isWholePropsObjectReference(analysis, ref)) continue;
34143
+ if (isCustomHookParameter(ref)) continue;
34144
+ } else continue;
34145
+ const methodName = getCallMethodName(calleeNode);
34146
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
34147
+ if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
34148
+ if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
34149
+ if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
34150
+ 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) ?? ""));
34151
+ const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
34152
+ const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
34153
+ if (isFunctionLike$1(argument)) {
34154
+ if (!isSetterNamedCallee) return [];
34155
+ return getFunctionalUpdaterDataRefs(analysis, argument);
34156
+ }
34157
+ if (isHandlerBagArgument(analysis, argument)) return [];
34158
+ if (isParentWiredHookResultArgument(analysis, argument)) return [];
34159
+ if (isNodeOfType(argument, "Identifier")) {
34160
+ const argumentRef = getRef(analysis, argument);
34161
+ if (argumentRef && resolveToFunction(argumentRef)) return [];
34162
+ }
34163
+ return getDownstreamRefs(analysis, argument);
34164
+ }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34165
+ if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34166
+ if (!argsUpstreamRefs.some((argRef) => {
34167
+ if (isUseStateIdentifier(argRef.identifier)) return false;
34168
+ if (isProp(analysis, argRef)) return false;
34169
+ if (isUseRefIdentifier(argRef.identifier)) return false;
34170
+ if (isRefCurrent(argRef)) return false;
34171
+ if (isConstant(argRef)) return false;
34172
+ if (isParentWiredHookResultRef(analysis, argRef)) return false;
34173
+ if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
34174
+ if (resolvesToFunctionBinding(argRef)) return false;
34175
+ const argIdentifier = argRef.identifier;
34176
+ if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
34177
+ if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
34178
+ return true;
34179
+ })) continue;
34180
+ context.report({
34181
+ node: callExpr,
34182
+ message: "Handing data back to a parent from a useEffect costs your users an extra render."
34183
+ });
34184
+ }
34185
+ } };
34186
+ }
33014
34187
  });
33015
34188
  //#endregion
33016
34189
  //#region src/plugin/utils/is-call-result-consumed-as-argument.ts
@@ -36272,6 +37445,140 @@ const noUnescapedEntities = defineRule({
36272
37445
  } })
36273
37446
  });
36274
37447
  //#endregion
37448
+ //#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
37449
+ const BROWSER_GLOBAL_NAMES = new Set([
37450
+ "window",
37451
+ "document",
37452
+ "localStorage",
37453
+ "sessionStorage",
37454
+ "navigator",
37455
+ "matchMedia"
37456
+ ]);
37457
+ const getTypeofBrowserGlobalName = (expression, context) => {
37458
+ const unwrappedExpression = stripParenExpression(expression);
37459
+ if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
37460
+ const argument = stripParenExpression(unwrappedExpression.argument);
37461
+ if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
37462
+ 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;
37463
+ return argument.property.name;
37464
+ };
37465
+ const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
37466
+ const mergeAvailability = (leftAvailability, rightAvailability) => {
37467
+ if (leftAvailability === null) return rightAvailability;
37468
+ if (rightAvailability === null) return leftAvailability;
37469
+ return leftAvailability === rightAvailability ? leftAvailability : null;
37470
+ };
37471
+ const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
37472
+ const unwrappedExpression = stripParenExpression(expression);
37473
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
37474
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
37475
+ if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
37476
+ if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
37477
+ return null;
37478
+ }
37479
+ if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
37480
+ const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
37481
+ const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
37482
+ const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
37483
+ const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
37484
+ const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
37485
+ const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
37486
+ if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
37487
+ if (!comparedType) return null;
37488
+ const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
37489
+ const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
37490
+ if (!isEquality && !isInequality) return null;
37491
+ const browserType = guardName === "matchMedia" ? "function" : "object";
37492
+ const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
37493
+ if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
37494
+ return predicateResult === browserResult;
37495
+ };
37496
+ const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
37497
+ let currentNode = node;
37498
+ let parentNode = currentNode.parent;
37499
+ while (parentNode) {
37500
+ if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
37501
+ if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
37502
+ if (isNodeOfType(parentNode, "ConditionalExpression")) {
37503
+ if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
37504
+ }
37505
+ if (isNodeOfType(parentNode, "IfStatement")) {
37506
+ if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
37507
+ }
37508
+ currentNode = parentNode;
37509
+ parentNode = currentNode.parent;
37510
+ }
37511
+ return false;
37512
+ };
37513
+ const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
37514
+ const enclosingFunction = findEnclosingFunction(node);
37515
+ if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
37516
+ let currentNode = node;
37517
+ while (currentNode !== enclosingFunction) {
37518
+ const parentNode = currentNode.parent;
37519
+ if (!parentNode) return false;
37520
+ if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
37521
+ if (statement === currentNode) break;
37522
+ if (!isNodeOfType(statement, "IfStatement")) continue;
37523
+ if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
37524
+ if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
37525
+ }
37526
+ currentNode = parentNode;
37527
+ }
37528
+ return false;
37529
+ };
37530
+ const isTypeofProbe = (node) => {
37531
+ const expressionRoot = findTransparentExpressionRoot(node);
37532
+ const parentNode = expressionRoot.parent;
37533
+ return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
37534
+ };
37535
+ const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
37536
+ id: "no-unguarded-browser-global-in-render-or-hook-init",
37537
+ title: "Browser global read during server render",
37538
+ severity: "error",
37539
+ category: "Correctness",
37540
+ requires: ["ssr"],
37541
+ 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.",
37542
+ create: (context) => {
37543
+ if (isTestlikeFilename(context.filename)) return {};
37544
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
37545
+ let fileIsEmailTemplate = false;
37546
+ const reportedNodes = /* @__PURE__ */ new Set();
37547
+ const reportBrowserRead = (node, browserGlobalName) => {
37548
+ if (reportedNodes.has(node) || isTypeofProbe(node)) return;
37549
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
37550
+ if (!componentOrHookNode) return;
37551
+ if (fileIsEmailTemplate) return;
37552
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
37553
+ if (isGatedByFalsyInitialState(node, context.scopes)) return;
37554
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
37555
+ if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
37556
+ if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
37557
+ reportedNodes.add(node);
37558
+ context.report({
37559
+ node,
37560
+ 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.`
37561
+ });
37562
+ };
37563
+ return {
37564
+ Program(node) {
37565
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
37566
+ },
37567
+ Identifier(node) {
37568
+ if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
37569
+ if (!context.scopes.isGlobalReference(node)) return;
37570
+ reportBrowserRead(node, node.name);
37571
+ },
37572
+ MemberExpression(node) {
37573
+ if (node.computed) return;
37574
+ const objectNode = stripParenExpression(node.object);
37575
+ if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
37576
+ reportBrowserRead(node, node.property.name);
37577
+ }
37578
+ };
37579
+ }
37580
+ });
37581
+ //#endregion
36275
37582
  //#region src/plugin/constants/dom-aria-properties.ts
36276
37583
  const ARIA_PROPERTY_NAMES = new Set([
36277
37584
  "activedescendant",
@@ -40114,6 +41421,123 @@ const preferUseEffectEvent = defineRule({
40114
41421
  }
40115
41422
  });
40116
41423
  //#endregion
41424
+ //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
41425
+ const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
41426
+ "each",
41427
+ "every",
41428
+ "filter",
41429
+ "find",
41430
+ "findIndex",
41431
+ "findLast",
41432
+ "findLastIndex",
41433
+ "flatMap",
41434
+ "forEach",
41435
+ "map",
41436
+ "reduce",
41437
+ "reduceRight",
41438
+ "some",
41439
+ "sort",
41440
+ "toSorted"
41441
+ ]);
41442
+ const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
41443
+ "from",
41444
+ "fromAsync",
41445
+ "groupBy"
41446
+ ]);
41447
+ const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
41448
+ const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
41449
+ const isListenerRemovalViaNullHandler = (callNode) => {
41450
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
41451
+ const callee = unwrapChainExpression(callNode.callee);
41452
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
41453
+ };
41454
+ const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
41455
+ "off",
41456
+ "removeEventListener",
41457
+ "removeListener"
41458
+ ]);
41459
+ const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
41460
+ if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
41461
+ const handlerArgument = callNode.arguments?.[1];
41462
+ return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
41463
+ };
41464
+ const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41465
+ const callNode = unwrapChainExpression(node);
41466
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
41467
+ if (isListenerRemovalViaNullHandler(callNode)) return true;
41468
+ const callee = unwrapChainExpression(callNode.callee);
41469
+ if (isNodeOfType(callee, "Identifier")) {
41470
+ if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
41471
+ if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
41472
+ if (knownCleanupFunctionNames.has(callee.name)) return true;
41473
+ return false;
41474
+ }
41475
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
41476
+ if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
41477
+ if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
41478
+ return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
41479
+ }
41480
+ return false;
41481
+ };
41482
+ 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);
41483
+ const isIteratorCallbackArgument = (node) => {
41484
+ const parentNode = node.parent;
41485
+ if (!isNodeOfType(parentNode, "CallExpression")) return false;
41486
+ if (!parentNode.arguments?.some((argument) => argument === node)) return false;
41487
+ const callee = unwrapChainExpression(parentNode.callee);
41488
+ if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
41489
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
41490
+ };
41491
+ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41492
+ let didFindRelease = false;
41493
+ walkAst(node, (child) => {
41494
+ if (didFindRelease) return false;
41495
+ if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
41496
+ if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
41497
+ didFindRelease = true;
41498
+ return false;
41499
+ }
41500
+ });
41501
+ return didFindRelease;
41502
+ };
41503
+ const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
41504
+ if (!isFunctionLike$1(node)) return false;
41505
+ return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
41506
+ };
41507
+ const isProvablyNoOpCleanupFunction = (node) => {
41508
+ if (!isFunctionLike$1(node)) return false;
41509
+ let sawNoOpRemoval = false;
41510
+ let sawOtherCall = false;
41511
+ walkAst(node.body, (child) => {
41512
+ if (sawOtherCall) return false;
41513
+ if (isFunctionLike$1(child)) return false;
41514
+ const callNode = unwrapChainExpression(child);
41515
+ if (!isNodeOfType(callNode, "CallExpression")) return;
41516
+ const callee = unwrapChainExpression(callNode.callee);
41517
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
41518
+ sawNoOpRemoval = true;
41519
+ return;
41520
+ }
41521
+ sawOtherCall = true;
41522
+ });
41523
+ return sawNoOpRemoval && !sawOtherCall;
41524
+ };
41525
+ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
41526
+ if (!returnedValue) return false;
41527
+ const unwrappedValue = unwrapChainExpression(returnedValue);
41528
+ if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
41529
+ if (isNodeOfType(unwrappedValue, "Identifier")) {
41530
+ if (unwrappedValue.name === "undefined") return false;
41531
+ if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
41532
+ return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
41533
+ }
41534
+ if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
41535
+ if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
41536
+ if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
41537
+ if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
41538
+ return false;
41539
+ };
41540
+ //#endregion
40117
41541
  //#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
40118
41542
  const findUseEffectsInComponent = (componentBody) => {
40119
41543
  const effectCalls = [];
@@ -41582,7 +43006,7 @@ const renderingHydrationMismatchTime = defineRule({
41582
43006
  title: "Time or random value in JSX",
41583
43007
  severity: "warn",
41584
43008
  category: "Correctness",
41585
- disabledWhen: ["vite", "cra"],
43009
+ requires: ["ssr"],
41586
43010
  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
43011
  create: (context) => {
41588
43012
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -41596,8 +43020,7 @@ const renderingHydrationMismatchTime = defineRule({
41596
43020
  const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
41597
43021
  if (matched) {
41598
43022
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
41599
- if (isInsideClientOnlyGuard(node)) return;
41600
- if (isGatedByFalsyInitialState(node)) return;
43023
+ if (isGatedByFalsyInitialState(node, context.scopes)) return;
41601
43024
  if (isInsideMotionTransitionAttribute(node)) return;
41602
43025
  context.report({
41603
43026
  node,
@@ -41606,11 +43029,10 @@ const renderingHydrationMismatchTime = defineRule({
41606
43029
  return;
41607
43030
  }
41608
43031
  walkAst(node.expression, (child) => {
41609
- if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
43032
+ if (isFunctionLike$1(child) && !executesDuringRender(child, context.scopes)) return false;
41610
43033
  for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
41611
43034
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
41612
- if (isInsideClientOnlyGuard(child)) return;
41613
- if (isGatedByFalsyInitialState(child)) return;
43035
+ if (isGatedByFalsyInitialState(child, context.scopes)) return;
41614
43036
  if (isInsideMotionTransitionAttribute(child)) return;
41615
43037
  if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
41616
43038
  context.report({
@@ -54947,6 +56369,18 @@ const reactDoctorRules = [
54947
56369
  category: "Accessibility"
54948
56370
  }
54949
56371
  },
56372
+ {
56373
+ key: "react-doctor/no-hydration-branch-on-browser-global",
56374
+ id: "no-hydration-branch-on-browser-global",
56375
+ source: "react-doctor",
56376
+ originallyExternal: false,
56377
+ rule: {
56378
+ ...noHydrationBranchOnBrowserGlobal,
56379
+ framework: "global",
56380
+ category: "Bugs",
56381
+ requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
56382
+ }
56383
+ },
54950
56384
  {
54951
56385
  key: "react-doctor/no-img-lazy-with-high-fetchpriority",
54952
56386
  id: "no-img-lazy-with-high-fetchpriority",
@@ -54959,6 +56393,18 @@ const reactDoctorRules = [
54959
56393
  requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
54960
56394
  }
54961
56395
  },
56396
+ {
56397
+ key: "react-doctor/no-impure-state-updater",
56398
+ id: "no-impure-state-updater",
56399
+ source: "react-doctor",
56400
+ originallyExternal: false,
56401
+ rule: {
56402
+ ...noImpureStateUpdater,
56403
+ framework: "global",
56404
+ category: "Bugs",
56405
+ requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
56406
+ }
56407
+ },
54962
56408
  {
54963
56409
  key: "react-doctor/no-indeterminate-attribute",
54964
56410
  id: "no-indeterminate-attribute",
@@ -55710,6 +57156,18 @@ const reactDoctorRules = [
55710
57156
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
55711
57157
  }
55712
57158
  },
57159
+ {
57160
+ key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
57161
+ id: "no-unguarded-browser-global-in-render-or-hook-init",
57162
+ source: "react-doctor",
57163
+ originallyExternal: false,
57164
+ rule: {
57165
+ ...noUnguardedBrowserGlobalInRenderOrHookInit,
57166
+ framework: "global",
57167
+ category: "Bugs",
57168
+ requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
57169
+ }
57170
+ },
55713
57171
  {
55714
57172
  key: "react-doctor/no-unknown-property",
55715
57173
  id: "no-unknown-property",
@@ -58042,10 +59500,17 @@ const CROSS_FILE_RULE_IDS = new Set([
58042
59500
  "nextjs-no-use-search-params-without-suspense",
58043
59501
  "no-dynamic-import-path",
58044
59502
  "no-full-lodash-import",
59503
+ "no-hydration-branch-on-browser-global",
58045
59504
  "no-indeterminate-attribute",
58046
59505
  "no-locale-format-in-render",
58047
59506
  "no-match-media-in-state-initializer",
59507
+ "no-adjust-state-on-prop-change",
59508
+ "no-derived-state",
59509
+ "no-derived-state-effect",
59510
+ "no-event-handler",
59511
+ "no-initialize-state",
58048
59512
  "no-mutating-reducer-state",
59513
+ "no-unguarded-browser-global-in-render-or-hook-init",
58049
59514
  "prefer-dynamic-import",
58050
59515
  "rendering-hydration-mismatch-time",
58051
59516
  "rn-no-legacy-shadow-styles",
@@ -58097,6 +59562,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
58097
59562
  if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
58098
59563
  for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
58099
59564
  };
59565
+ const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
59566
+ for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
59567
+ };
58100
59568
  const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
58101
59569
  const namedUseReducerLocals = /* @__PURE__ */ new Set();
58102
59570
  const reactObjectLocals = /* @__PURE__ */ new Set();
@@ -58179,10 +59647,17 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
58179
59647
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
58180
59648
  ["no-dynamic-import-path", collectNearestManifestDependencies],
58181
59649
  ["no-full-lodash-import", collectNearestManifestDependencies],
59650
+ ["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
58182
59651
  ["no-indeterminate-attribute", collectNearestManifestDependencies],
58183
59652
  ["no-locale-format-in-render", collectNearestManifestDependencies],
58184
59653
  ["no-match-media-in-state-initializer", collectNearestManifestDependencies],
59654
+ ["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
59655
+ ["no-derived-state", collectEffectValueHelperDependencies],
59656
+ ["no-derived-state-effect", collectEffectValueHelperDependencies],
59657
+ ["no-event-handler", collectEffectValueHelperDependencies],
59658
+ ["no-initialize-state", collectEffectValueHelperDependencies],
58185
59659
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
59660
+ ["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
58186
59661
  ["prefer-dynamic-import", collectNearestManifestDependencies],
58187
59662
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
58188
59663
  ["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],