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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +486 -907
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8406,54 +8406,6 @@ const effectListenerCleanupMismatch = defineRule({
8406
8406
  } })
8407
8407
  });
8408
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
8457
8409
  //#region src/plugin/utils/is-react-hook-name.ts
8458
8410
  const isReactHookName = (name) => {
8459
8411
  if (!name.startsWith("use")) return false;
@@ -8509,90 +8461,46 @@ const enclosingComponentOrHookName = (node) => {
8509
8461
  return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
8510
8462
  };
8511
8463
  //#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
8588
8464
  //#region src/plugin/utils/get-range-start.ts
8589
8465
  const getRangeStart = (node) => {
8590
8466
  const rangeStart = node.range?.[0];
8591
8467
  return typeof rangeStart === "number" ? rangeStart : null;
8592
8468
  };
8593
8469
  //#endregion
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);
8470
+ //#region src/plugin/utils/is-result-discarded-call.ts
8471
+ const isResultDiscardedCall = (callExpression) => {
8472
+ let node = callExpression;
8473
+ let parent = node.parent;
8474
+ while (parent) {
8475
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
8476
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
8477
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
8478
+ if (isNodeOfType(parent, "ChainExpression")) {
8479
+ node = parent;
8480
+ parent = node.parent;
8481
+ continue;
8482
+ }
8483
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
8484
+ node = parent;
8485
+ parent = node.parent;
8486
+ continue;
8487
+ }
8488
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
8489
+ node = parent;
8490
+ parent = node.parent;
8491
+ continue;
8492
+ }
8493
+ if (isNodeOfType(parent, "SequenceExpression")) {
8494
+ const expressions = parent.expressions ?? [];
8495
+ if (expressions[expressions.length - 1] !== node) return true;
8496
+ node = parent;
8497
+ parent = node.parent;
8498
+ continue;
8499
+ }
8500
+ return false;
8501
+ }
8502
+ return false;
8503
+ };
8596
8504
  //#endregion
8597
8505
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
8598
8506
  const walkInsideStatementBlocks = (node, visitor) => {
@@ -8627,79 +8535,136 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
8627
8535
  return true;
8628
8536
  };
8629
8537
  //#endregion
8630
- //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
8631
- const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
8632
- const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
8633
- const RESOURCE_NOUN_BY_KIND = {
8634
- subscribe: "subscription",
8635
- timer: "timer",
8636
- socket: "connection"
8538
+ //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
8539
+ const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
8540
+ "each",
8541
+ "every",
8542
+ "filter",
8543
+ "find",
8544
+ "findIndex",
8545
+ "findLast",
8546
+ "findLastIndex",
8547
+ "flatMap",
8548
+ "forEach",
8549
+ "map",
8550
+ "reduce",
8551
+ "reduceRight",
8552
+ "some",
8553
+ "sort",
8554
+ "toSorted"
8555
+ ]);
8556
+ const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
8557
+ "from",
8558
+ "fromAsync",
8559
+ "groupBy"
8560
+ ]);
8561
+ const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
8562
+ const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
8563
+ const isListenerRemovalViaNullHandler = (callNode) => {
8564
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
8565
+ const callee = unwrapChainExpression$3(callNode.callee);
8566
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
8637
8567
  };
8638
- const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
8639
- const isSubscribeOrObserveCall = (node) => {
8640
- if (isSubscribeLikeCallExpression(node)) return true;
8641
- return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
8568
+ const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
8569
+ "off",
8570
+ "removeEventListener",
8571
+ "removeListener"
8572
+ ]);
8573
+ const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
8574
+ if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
8575
+ const handlerArgument = callNode.arguments?.[1];
8576
+ return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
8642
8577
  };
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;
8578
+ const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8579
+ const callNode = unwrapChainExpression$3(node);
8580
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
8581
+ if (isListenerRemovalViaNullHandler(callNode)) return true;
8582
+ const callee = unwrapChainExpression$3(callNode.callee);
8583
+ if (isNodeOfType(callee, "Identifier")) {
8584
+ if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
8585
+ if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
8586
+ if (knownCleanupFunctionNames.has(callee.name)) return true;
8587
+ return false;
8667
8588
  }
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}`;
8589
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
8590
+ if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
8591
+ if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
8592
+ return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
8673
8593
  }
8674
- return null;
8594
+ return false;
8675
8595
  };
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;
8596
+ const isStaticIteratorCallbackCallee = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && (callee.object.name === "Array" || callee.object.name === "Map" || callee.object.name === "Object") && STATIC_ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
8597
+ const isIteratorCallbackArgument = (node) => {
8598
+ const parentNode = node.parent;
8599
+ if (!isNodeOfType(parentNode, "CallExpression")) return false;
8600
+ if (!parentNode.arguments?.some((argument) => argument === node)) return false;
8601
+ const callee = unwrapChainExpression$3(parentNode.callee);
8602
+ if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
8603
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(callee.property.name);
8604
+ };
8605
+ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8606
+ let didFindRelease = false;
8607
+ walkAst(node, (child) => {
8608
+ if (didFindRelease) return false;
8609
+ if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
8610
+ if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
8611
+ didFindRelease = true;
8612
+ return false;
8613
+ }
8614
+ });
8615
+ return didFindRelease;
8616
+ };
8617
+ const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
8618
+ if (!isFunctionLike$1(node)) return false;
8619
+ return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
8620
+ };
8621
+ const isProvablyNoOpCleanupFunction = (node) => {
8622
+ if (!isFunctionLike$1(node)) return false;
8623
+ let sawNoOpRemoval = false;
8624
+ let sawOtherCall = false;
8625
+ walkAst(node.body, (child) => {
8626
+ if (sawOtherCall) return false;
8627
+ if (isFunctionLike$1(child)) return false;
8628
+ const callNode = unwrapChainExpression$3(child);
8629
+ if (!isNodeOfType(callNode, "CallExpression")) return;
8630
+ const callee = unwrapChainExpression$3(callNode.callee);
8631
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
8632
+ sawNoOpRemoval = true;
8633
+ return;
8634
+ }
8635
+ sawOtherCall = true;
8636
+ });
8637
+ return sawNoOpRemoval && !sawOtherCall;
8638
+ };
8639
+ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
8640
+ if (!returnedValue) return false;
8641
+ const unwrappedValue = unwrapChainExpression$3(returnedValue);
8642
+ if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
8643
+ if (isNodeOfType(unwrappedValue, "Identifier")) {
8644
+ if (unwrappedValue.name === "undefined") return false;
8645
+ if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
8646
+ return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
8682
8647
  }
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;
8648
+ if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
8649
+ if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
8650
+ if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
8651
+ if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
8652
+ return false;
8653
+ };
8654
+ //#endregion
8655
+ //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
8656
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
8657
+ const RESOURCE_NOUN_BY_KIND = {
8658
+ subscribe: "subscription",
8659
+ timer: "timer",
8660
+ socket: "connection"
8686
8661
  };
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
- };
8662
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
8663
+ const isSubscribeOrObserveCall = (node) => {
8664
+ if (isSubscribeLikeCallExpression(node)) return true;
8665
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
8701
8666
  };
8702
- const findSubscribeLikeUsages = (callback, context) => {
8667
+ const findSubscribeLikeUsages = (callback) => {
8703
8668
  const usages = [];
8704
8669
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8705
8670
  let cleanupArgument = null;
@@ -8708,22 +8673,13 @@ const findSubscribeLikeUsages = (callback, context) => {
8708
8673
  const lastCallbackStatement = callbackStatements[callbackStatements.length - 1];
8709
8674
  if (isNodeOfType(lastCallbackStatement, "ReturnStatement") && lastCallbackStatement.argument) cleanupArgument = lastCallbackStatement.argument;
8710
8675
  }
8711
- const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
8712
8676
  walkAst(callback, (child) => {
8713
- if (child !== callback && isFunctionLike$1(child)) {
8714
- if (child === cleanupArgument) return false;
8715
- if (!effectInvokedFunctions.has(child) && !isSynchronousIteratorCallback(child)) return false;
8716
- }
8677
+ if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
8717
8678
  if (isSocketConstruction(child)) {
8718
8679
  usages.push({
8719
8680
  kind: "socket",
8720
8681
  node: child,
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
8682
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8727
8683
  });
8728
8684
  return;
8729
8685
  }
@@ -8732,348 +8688,118 @@ const findSubscribeLikeUsages = (callback, context) => {
8732
8688
  usages.push({
8733
8689
  kind: "timer",
8734
8690
  node: child,
8735
- resourceName: child.callee.name,
8736
- handleKey: findAssignedResourceKey(child, context),
8737
- receiverKey: null,
8738
- registrationVerbName: child.callee.name,
8739
- eventKey: null,
8740
- handlerKey: null
8691
+ resourceName: child.callee.name
8741
8692
  });
8742
8693
  return;
8743
8694
  }
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
- });
8695
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
8696
+ kind: "subscribe",
8697
+ node: child,
8698
+ resourceName: child.callee.property.name
8699
+ });
8700
+ });
8701
+ return usages;
8702
+ };
8703
+ const collectCleanupBindings = (effectCallback) => {
8704
+ const bindings = {
8705
+ cleanupFunctionNames: /* @__PURE__ */ new Set(),
8706
+ subscriptionNames: /* @__PURE__ */ new Set(),
8707
+ effectScopeVariableNames: /* @__PURE__ */ new Set()
8708
+ };
8709
+ if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
8710
+ if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
8711
+ walkAst(effectCallback.body, (child) => {
8712
+ if (!isSubscribeOrObserveCall(child)) return;
8713
+ if (!isNodeOfType(child, "CallExpression")) return;
8714
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
8715
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
8716
+ bindings.subscriptionNames.add(child.callee.object.name);
8717
+ });
8718
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
8719
+ if (!isNodeOfType(child, "VariableDeclaration")) return;
8720
+ for (const declarator of child.declarations ?? []) {
8721
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
8722
+ const bindingName = declarator.id.name;
8723
+ bindings.effectScopeVariableNames.add(bindingName);
8724
+ const init = declarator.init;
8725
+ if (!init) continue;
8726
+ if (isSocketConstruction(init)) {
8727
+ bindings.subscriptionNames.add(bindingName);
8728
+ continue;
8729
+ }
8730
+ if (!isNodeOfType(init, "CallExpression")) continue;
8731
+ if (isSubscribeLikeCallExpression(init)) {
8732
+ bindings.subscriptionNames.add(bindingName);
8733
+ if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
8734
+ }
8753
8735
  }
8754
8736
  });
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) => {
8737
+ walkAst(effectCallback.body, (child) => {
8738
+ if (child !== effectCallback.body && (isNodeOfType(child, "ArrowFunctionExpression") || isNodeOfType(child, "FunctionExpression"))) return false;
8739
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id && isCleanupFunctionLike(child, bindings.cleanupFunctionNames, bindings.subscriptionNames)) {
8740
+ bindings.cleanupFunctionNames.add(child.id.name);
8741
+ return false;
8742
+ }
8743
+ });
8744
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
8745
+ if (!isNodeOfType(child, "VariableDeclaration")) return;
8746
+ for (const declarator of child.declarations ?? []) {
8747
+ if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
8748
+ if (isCleanupFunctionLike(declarator.init, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(declarator.id.name);
8749
+ }
8750
+ });
8751
+ walkAst(effectCallback.body, (child) => {
8752
+ if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && bindings.effectScopeVariableNames.has(child.left.name) && isCleanupFunctionLike(child.right, bindings.cleanupFunctionNames, bindings.subscriptionNames)) bindings.cleanupFunctionNames.add(child.left.name);
8753
+ });
8754
+ return bindings;
8755
+ };
8756
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8820
8757
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8821
8758
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8822
- const releaseCalls = [];
8759
+ const releaseStarts = [];
8823
8760
  walkInsideStatementBlocks(callback.body, (child) => {
8824
- if (!isNodeOfType(isNodeOfType(child, "ChainExpression") ? child.expression : child, "CallExpression")) return;
8825
- releaseCalls.push(child);
8761
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8762
+ const releaseStart = getRangeStart(child);
8763
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8826
8764
  });
8827
- if (releaseCalls.length === 0) return usages;
8765
+ if (releaseStarts.length === 0) return usages;
8828
8766
  return usages.filter((usage) => {
8829
8767
  const usageStart = getRangeStart(usage.node);
8830
8768
  if (usageStart === null) return true;
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);
8836
- });
8837
- };
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] : [];
8769
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8972
8770
  });
8973
- if (invocationCalls.length !== 1) return null;
8974
- const invocationCall = invocationCalls[0];
8975
- return findEnclosingFunction(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
8976
8771
  };
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
- }
8772
+ const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
8773
+ if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
8774
+ const returnStart = getRangeStart(returnStatement);
8775
+ if (returnStart === null) return true;
8776
+ return usages.some((usage) => {
8777
+ const usageStart = getRangeStart(usage.node);
8778
+ return usageStart === null || usageStart < returnStart;
9016
8779
  });
9017
- return didCleanupFunctionMatch;
9018
8780
  };
9019
- const effectHasCleanupForUsage = (callback, usage, context) => {
8781
+ const effectHasCleanupReturn = (callback, usages) => {
9020
8782
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return 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
+ if (!isNodeOfType(callback.body, "BlockStatement")) return isCleanupReturningSubscribeLikeCallExpression(callback.body);
8784
+ const cleanupBindings = collectCleanupBindings(callback);
8785
+ let didFindCleanupReturn = false;
9024
8786
  walkInsideStatementBlocks(callback.body, (child) => {
8787
+ if (didFindCleanupReturn) return;
9025
8788
  if (!isNodeOfType(child, "ReturnStatement")) return;
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);
8789
+ if (!cleanupReturnRunsAfterUsage(child, usages)) return;
8790
+ if (isCleanupReturn(child.argument, cleanupBindings.cleanupFunctionNames, cleanupBindings.subscriptionNames, { allowOpaqueReturn: true })) didFindCleanupReturn = true;
9048
8791
  });
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));
8792
+ return didFindCleanupReturn;
9068
8793
  };
9069
- const isSelfReleasingListenerOptionProperty = (property, context) => {
8794
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8795
+ const isSelfReleasingListenerOptionProperty = (property) => {
9070
8796
  if (!isNodeOfType(property, "Property")) return false;
9071
8797
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
9072
- if (keyName === "signal") return !isLocalAbortControllerExpression(property.value, context);
8798
+ if (keyName === "signal") return true;
9073
8799
  if (keyName !== "once") return false;
9074
8800
  return isNodeOfType(property.value, "Literal") && property.value.value === true;
9075
8801
  };
9076
- const hasSelfReleasingListenerOptions = (node, context) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isSelfReleasingListenerOptionProperty(property, context)));
8802
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
9077
8803
  const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
9078
8804
  ["addEventListener", new Set(["removeEventListener", "abort"])],
9079
8805
  ["addListener", new Set([
@@ -9099,185 +8825,85 @@ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
9099
8825
  "teardown"
9100
8826
  ]);
9101
8827
  const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8828
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
9102
8829
  const getReleaseVerbName = (node) => {
8830
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
9103
8831
  const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
9104
8832
  if (!isNodeOfType(callNode, "CallExpression")) return null;
9105
8833
  const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
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
- }
8834
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8835
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9111
8836
  return null;
9112
8837
  };
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
- };
9153
8838
  const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
9154
- const isReturnedEffectCleanupFunction = (functionNode) => {
9155
- let currentNode = functionNode;
9156
- let parentNode = currentNode.parent;
9157
- while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
9158
- currentNode = parentNode;
9159
- parentNode = currentNode.parent;
9160
- }
9161
- if (!isNodeOfType(parentNode, "ReturnStatement") || parentNode.argument !== currentNode) return false;
9162
- const effectCallback = findEnclosingFunction(parentNode);
9163
- const effectCall = effectCallback?.parent;
9164
- return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
9165
- };
9166
- const isPotentiallyReachableFunction = (functionNode, context) => {
9167
- if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode)) return true;
9168
- const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
9169
- if (!bindingIdentifier) return false;
9170
- const symbol = context.scopes.symbolFor(bindingIdentifier);
9171
- if (!symbol) return false;
9172
- return symbol.references.some((reference) => findEnclosingFunction(reference.identifier) !== functionNode);
9173
- };
9174
- const isReleaseReachableForUsage = (releaseNode, usage, context) => {
9175
- if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
9176
- const releaseFunction = findEnclosingFunction(releaseNode);
9177
- if (!releaseFunction) return true;
9178
- if (releaseFunction === findEnclosingFunction(usage.node)) return true;
9179
- return isPotentiallyReachableFunction(releaseFunction, context);
9180
- };
9181
- const fileContainsReleaseForUsage = (usage, context) => {
9182
- let programNode = usage.node;
9183
- while (programNode.parent) programNode = programNode.parent;
9184
- let didFindRelease = false;
9185
- walkAst(programNode, (child) => {
9186
- if (didFindRelease) return false;
9187
- if (doesReleaseCallMatchUsage(child, usage, context) && isReleaseReachableForUsage(child, usage, context)) {
9188
- didFindRelease = true;
8839
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8840
+ let didFindPairedRelease = false;
8841
+ walkAst(body, (child) => {
8842
+ if (didFindPairedRelease) return false;
8843
+ if (child !== body && isFunctionLike$1(child)) return false;
8844
+ const releaseVerbName = getReleaseVerbName(child);
8845
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8846
+ didFindPairedRelease = true;
9189
8847
  return false;
9190
8848
  }
9191
8849
  });
9192
- return didFindRelease;
8850
+ return didFindPairedRelease;
9193
8851
  };
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);
8852
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8853
+ const collectFileReleaseVerbNames = (anyNode) => {
8854
+ let programNode = anyNode;
8855
+ while (programNode.parent) programNode = programNode.parent;
8856
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8857
+ if (cached) return cached;
8858
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8859
+ walkAst(programNode, (child) => {
8860
+ const releaseVerbName = getReleaseVerbName(child);
8861
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8862
+ });
8863
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8864
+ return releaseVerbNames;
9211
8865
  };
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
- }
8866
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8867
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8868
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8869
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8870
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
9225
8871
  return false;
9226
8872
  };
9227
- const findRetainedFunctionLeak = (retainedFunction, context) => {
8873
+ const findRetainedFunctionLeak = (retainedFunction) => {
9228
8874
  if (!isFunctionLike$1(retainedFunction)) return null;
9229
8875
  const body = retainedFunction.body;
9230
8876
  if (!body) return null;
8877
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
9231
8878
  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);
9235
8879
  walkAst(body, (child) => {
9236
8880
  if (leak !== null) return false;
9237
8881
  if (isFunctionLike$1(child)) return false;
9238
- if (isSocketConstruction(child) && !doesResourceResultEscape(child, false)) {
9239
- const socketUsage = {
8882
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8883
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8884
+ leak = {
9240
8885
  kind: "socket",
9241
8886
  node: child,
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
8887
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
9248
8888
  };
9249
- if (!hasReleaseForUsage(socketUsage)) {
9250
- leak = socketUsage;
9251
- return false;
9252
- }
8889
+ return false;
9253
8890
  }
9254
8891
  if (!isNodeOfType(child, "CallExpression")) return;
9255
- if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
9256
- const timerUsage = {
8892
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8893
+ leak = {
9257
8894
  kind: "timer",
9258
8895
  node: child,
9259
- resourceName: "setInterval",
9260
- handleKey: findAssignedResourceKey(child, context),
9261
- receiverKey: null,
9262
- registrationVerbName: "setInterval",
9263
- eventKey: null,
9264
- handlerKey: null
8896
+ resourceName: "setInterval"
9265
8897
  };
9266
- if (!hasReleaseForUsage(timerUsage)) {
9267
- leak = timerUsage;
9268
- return false;
9269
- }
8898
+ return false;
9270
8899
  }
9271
- if (isSubscribeOrObserveCall(child) && !doesResourceResultEscape(child, allowConciseReturnEscape)) {
9272
- const registrationDetails = getCallRegistrationDetails(child, context);
9273
- const subscriptionUsage = {
8900
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8901
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8902
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
9274
8903
  kind: "subscribe",
9275
8904
  node: child,
9276
- resourceName: registrationDetails.registrationVerbName ?? "subscribe",
9277
- handleKey: findAssignedResourceKey(child, context),
9278
- ...registrationDetails
8905
+ resourceName: registrationVerbName
9279
8906
  };
9280
- if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
9281
8907
  return false;
9282
8908
  }
9283
8909
  });
@@ -9289,26 +8915,6 @@ const isRetainedComponentScopeFunction = (functionNode) => {
9289
8915
  if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
9290
8916
  return enclosingComponentOrHookName(functionNode) !== null;
9291
8917
  };
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
- };
9312
8918
  const effectNeedsCleanup = defineRule({
9313
8919
  id: "effect-needs-cleanup",
9314
8920
  title: "Effect subscription or timer never cleaned up",
@@ -9317,8 +8923,7 @@ const effectNeedsCleanup = defineRule({
9317
8923
  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.",
9318
8924
  create: (context) => {
9319
8925
  const reportRetainedLeak = (retainedFunction) => {
9320
- if (!isPotentiallyReachableFunction(retainedFunction, context)) return;
9321
- const leak = findRetainedFunctionLeak(retainedFunction, context);
8926
+ const leak = findRetainedFunctionLeak(retainedFunction);
9322
8927
  if (!leak) return;
9323
8928
  const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
9324
8929
  context.report({
@@ -9330,31 +8935,30 @@ const effectNeedsCleanup = defineRule({
9330
8935
  CallExpression(node) {
9331
8936
  if (isHookCall$2(node, "useCallback")) {
9332
8937
  const retainedCallback = getEffectCallback(node);
9333
- if (retainedCallback && !isInlineRetainedHandlerFunction(retainedCallback, context)) reportRetainedLeak(retainedCallback);
8938
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
9334
8939
  return;
9335
8940
  }
9336
- if (!isHookCall$2(node, CLEANUP_EFFECT_HOOK_NAMES)) return;
8941
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
9337
8942
  const callback = getEffectCallback(node);
9338
8943
  if (!callback) return;
9339
- const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback, context), context);
8944
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
9340
8945
  if (usages.length === 0) return;
9341
- const firstUsage = findFirstUsageWithoutCleanup(callback, usages, context);
9342
- if (!firstUsage) return;
8946
+ if (effectHasCleanupReturn(callback, usages)) return;
8947
+ const firstUsage = usages[0];
9343
8948
  const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
9344
- const hookName = getCalleeName$2(node) ?? "effect";
9345
8949
  context.report({
9346
8950
  node,
9347
- message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8951
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
9348
8952
  });
9349
8953
  },
9350
8954
  FunctionDeclaration(node) {
9351
8955
  if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9352
8956
  },
9353
8957
  ArrowFunctionExpression(node) {
9354
- if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
8958
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9355
8959
  },
9356
8960
  FunctionExpression(node) {
9357
- if (isRetainedComponentScopeFunction(node) || isInlineRetainedHandlerFunction(node, context)) reportRetainedLeak(node);
8961
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
9358
8962
  }
9359
8963
  };
9360
8964
  }
@@ -12738,6 +12342,20 @@ const collectHandlerReferencedNames = (root) => {
12738
12342
  return names;
12739
12343
  };
12740
12344
  //#endregion
12345
+ //#region src/plugin/utils/get-function-binding-name.ts
12346
+ const getFunctionBindingIdentifier = (functionNode) => {
12347
+ if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
12348
+ const parent = functionNode.parent;
12349
+ if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
12350
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.right === functionNode && isNodeOfType(parent.left, "Identifier")) return parent.left;
12351
+ if (isNodeOfType(parent, "CallExpression")) {
12352
+ const callParent = parent.parent;
12353
+ if (isNodeOfType(callParent, "VariableDeclarator") && isNodeOfType(callParent.id, "Identifier")) return callParent.id;
12354
+ }
12355
+ return null;
12356
+ };
12357
+ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(functionNode)?.name ?? null;
12358
+ //#endregion
12741
12359
  //#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
12742
12360
  const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
12743
12361
  const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -13882,7 +13500,7 @@ const jsHoistIntl = defineRule({
13882
13500
  });
13883
13501
  //#endregion
13884
13502
  //#region src/plugin/utils/create-loop-aware-visitors.ts
13885
- const ITERATOR_CALLBACK_METHOD_NAMES$1 = new Set([
13503
+ const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
13886
13504
  "map",
13887
13505
  "flatMap",
13888
13506
  "forEach",
@@ -13901,7 +13519,7 @@ const isIteratorCallback = (node) => {
13901
13519
  const parent = node.parent;
13902
13520
  if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
13903
13521
  if (!parent.arguments.includes(node)) return false;
13904
- return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES$1.has(parent.callee.property.name);
13522
+ return isNodeOfType(parent.callee, "MemberExpression") && isNodeOfType(parent.callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(parent.callee.property.name);
13905
13523
  };
13906
13524
  const createLoopAwareVisitors = (innerVisitors, options = {}) => {
13907
13525
  let loopDepth = 0;
@@ -14130,7 +13748,7 @@ const findIndexedArrayObject = (callbackBody, indexParameterName) => {
14130
13748
  });
14131
13749
  return indexedArrayObject;
14132
13750
  };
14133
- const unwrapChainExpression$3 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
13751
+ const unwrapChainExpression$2 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
14134
13752
  const LENGTH_EQUALITY_OPERATORS = new Set(["===", "=="]);
14135
13753
  const LENGTH_MISMATCH_OPERATORS = new Set([
14136
13754
  "!==",
@@ -14142,17 +13760,17 @@ const LENGTH_MISMATCH_OPERATORS = new Set([
14142
13760
  ]);
14143
13761
  const LENGTH_ANY_COMPARISON_OPERATORS = new Set([...LENGTH_EQUALITY_OPERATORS, ...LENGTH_MISMATCH_OPERATORS]);
14144
13762
  const isLengthComparison = (candidate, receiverArray, indexedArray, operators) => {
14145
- const binaryGuard = unwrapChainExpression$3(candidate);
13763
+ const binaryGuard = unwrapChainExpression$2(candidate);
14146
13764
  if (!isNodeOfType(binaryGuard, "BinaryExpression")) return false;
14147
13765
  if (!operators.has(binaryGuard.operator)) return false;
14148
- const leftSide = unwrapChainExpression$3(binaryGuard.left);
14149
- const rightSide = unwrapChainExpression$3(binaryGuard.right);
13766
+ const leftSide = unwrapChainExpression$2(binaryGuard.left);
13767
+ const rightSide = unwrapChainExpression$2(binaryGuard.right);
14150
13768
  if (!isMemberProperty(leftSide, "length")) return false;
14151
13769
  if (!isMemberProperty(rightSide, "length")) return false;
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);
13770
+ const leftLengthObject = unwrapChainExpression$2(leftSide.object);
13771
+ const rightLengthObject = unwrapChainExpression$2(rightSide.object);
13772
+ const normalizedReceiver = unwrapChainExpression$2(receiverArray);
13773
+ const normalizedIndexed = unwrapChainExpression$2(indexedArray);
14156
13774
  const matchesReceiverThenIndexed = areExpressionsStructurallyEqual(leftLengthObject, normalizedReceiver) && areExpressionsStructurallyEqual(rightLengthObject, normalizedIndexed);
14157
13775
  const matchesIndexedThenReceiver = areExpressionsStructurallyEqual(leftLengthObject, normalizedIndexed) && areExpressionsStructurallyEqual(rightLengthObject, normalizedReceiver);
14158
13776
  return matchesReceiverThenIndexed || matchesIndexedThenReceiver;
@@ -14239,18 +13857,18 @@ const LENGTH_PRESERVING_METHOD_NAMES = new Set([
14239
13857
  "toReversed"
14240
13858
  ]);
14241
13859
  const peelLengthPreservingDerivation = (expression) => {
14242
- let current = unwrapChainExpression$3(expression);
13860
+ let current = unwrapChainExpression$2(expression);
14243
13861
  for (;;) {
14244
13862
  if (isNodeOfType(current, "ArrayExpression") && current.elements?.length === 1 && isNodeOfType(current.elements[0], "SpreadElement")) {
14245
- current = unwrapChainExpression$3(current.elements[0].argument);
13863
+ current = unwrapChainExpression$2(current.elements[0].argument);
14246
13864
  continue;
14247
13865
  }
14248
13866
  if (isNodeOfType(current, "CallExpression")) {
14249
- const callee = unwrapChainExpression$3(current.callee);
13867
+ const callee = unwrapChainExpression$2(current.callee);
14250
13868
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
14251
- const calleeObject = unwrapChainExpression$3(callee.object);
13869
+ const calleeObject = unwrapChainExpression$2(callee.object);
14252
13870
  if (isNodeOfType(calleeObject, "Identifier") && calleeObject.name === "Array" && callee.property.name === "from" && current.arguments?.length === 1) {
14253
- current = unwrapChainExpression$3(current.arguments[0]);
13871
+ current = unwrapChainExpression$2(current.arguments[0]);
14254
13872
  continue;
14255
13873
  }
14256
13874
  if (LENGTH_PRESERVING_METHOD_NAMES.has(callee.property.name)) {
@@ -14295,7 +13913,7 @@ const resolveComparedArraySource = (expression, scopeNode) => {
14295
13913
  const initializer = findConstInitializer(current.name, scopeNode);
14296
13914
  if (!initializer) return current;
14297
13915
  const peeledInitializer = peelLengthPreservingDerivation(initializer);
14298
- if (peeledInitializer === unwrapChainExpression$3(initializer)) return current;
13916
+ if (peeledInitializer === unwrapChainExpression$2(initializer)) return current;
14299
13917
  current = peeledInitializer;
14300
13918
  }
14301
13919
  return current;
@@ -19989,41 +19607,89 @@ const nextjsMissingMetadata = defineRule({
19989
19607
  node: programNode,
19990
19608
  message: "This page has no metadata, so search engines and social previews get no title or description."
19991
19609
  });
19992
- } })
19993
- });
19994
- //#endregion
19995
- //#region src/plugin/rules/nextjs/nextjs-no-a-element.ts
19996
- const getJsxAttributeLiteralValue = (attributeValue) => {
19997
- if (!attributeValue) return void 0;
19998
- if (isNodeOfType(attributeValue, "Literal")) return attributeValue.value;
19999
- if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal")) return attributeValue.expression.value;
19610
+ } })
19611
+ });
19612
+ //#endregion
19613
+ //#region src/plugin/rules/nextjs/nextjs-no-a-element.ts
19614
+ const getJsxAttributeLiteralValue = (attributeValue) => {
19615
+ if (!attributeValue) return void 0;
19616
+ if (isNodeOfType(attributeValue, "Literal")) return attributeValue.value;
19617
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal")) return attributeValue.expression.value;
19618
+ };
19619
+ const nextjsNoAElement = defineRule({
19620
+ id: "nextjs-no-a-element",
19621
+ title: "Plain anchor reloads internal Next.js links",
19622
+ tags: ["test-noise"],
19623
+ requires: ["nextjs"],
19624
+ severity: "warn",
19625
+ recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
19626
+ create: (context) => ({ JSXOpeningElement(node) {
19627
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
19628
+ const attributes = node.attributes ?? [];
19629
+ const downloadAttribute = findJsxAttribute(attributes, "download");
19630
+ if (downloadAttribute) {
19631
+ if (!downloadAttribute.value) return;
19632
+ const downloadValue = getJsxAttributeLiteralValue(downloadAttribute.value);
19633
+ if (downloadValue !== false && downloadValue !== null) return;
19634
+ }
19635
+ if (getJsxAttributeLiteralValue(findJsxAttribute(attributes, "target")?.value) === "_blank") return;
19636
+ const hrefAttribute = findJsxAttribute(attributes, "href");
19637
+ if (!hrefAttribute?.value) return;
19638
+ const hrefValue = getJsxAttributeLiteralValue(hrefAttribute.value);
19639
+ if (typeof hrefValue === "string" && hrefValue.startsWith("/") && !hrefValue.startsWith("//")) context.report({
19640
+ node,
19641
+ message: "Plain <a> reloads the whole page for internal links, so Next.js loses client-side navigation and prefetching."
19642
+ });
19643
+ } })
19644
+ });
19645
+ //#endregion
19646
+ //#region src/plugin/utils/collect-effect-invoked-functions.ts
19647
+ const PROMISE_CHAIN_METHOD_NAMES$1 = new Set([
19648
+ "then",
19649
+ "catch",
19650
+ "finally"
19651
+ ]);
19652
+ const collectEffectInvokedFunctions = (effectCallback) => {
19653
+ const invokedFunctions = new Set([effectCallback]);
19654
+ const localFunctionBindings = /* @__PURE__ */ new Map();
19655
+ const calledBindingNames = /* @__PURE__ */ new Set();
19656
+ const pendingFunctions = [effectCallback];
19657
+ const enqueue = (candidate) => {
19658
+ const strippedCandidate = candidate ? stripParenExpression(candidate) : candidate;
19659
+ if (!isFunctionLike$1(strippedCandidate) || invokedFunctions.has(strippedCandidate)) return;
19660
+ invokedFunctions.add(strippedCandidate);
19661
+ pendingFunctions.push(strippedCandidate);
19662
+ };
19663
+ const isPromiseChainCall = (callee) => isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CHAIN_METHOD_NAMES$1.has(callee.property.name) && isNodeOfType(stripParenExpression(callee.object), "CallExpression");
19664
+ while (pendingFunctions.length > 0) {
19665
+ const currentFunction = pendingFunctions.pop();
19666
+ if (!currentFunction) break;
19667
+ walkAst(currentFunction, (child) => {
19668
+ if (child !== currentFunction && isFunctionLike$1(child)) {
19669
+ if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) localFunctionBindings.set(child.id.name, child);
19670
+ return false;
19671
+ }
19672
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
19673
+ const initializer = child.init ? stripParenExpression(child.init) : null;
19674
+ if (isFunctionLike$1(initializer)) localFunctionBindings.set(child.id.name, initializer);
19675
+ return;
19676
+ }
19677
+ if (!isNodeOfType(child, "CallExpression")) return;
19678
+ const callee = stripParenExpression(child.callee);
19679
+ if (isFunctionLike$1(callee)) {
19680
+ enqueue(callee);
19681
+ return;
19682
+ }
19683
+ if (isNodeOfType(callee, "Identifier")) {
19684
+ calledBindingNames.add(callee.name);
19685
+ return;
19686
+ }
19687
+ if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
19688
+ });
19689
+ for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
19690
+ }
19691
+ return invokedFunctions;
20000
19692
  };
20001
- const nextjsNoAElement = defineRule({
20002
- id: "nextjs-no-a-element",
20003
- title: "Plain anchor reloads internal Next.js links",
20004
- tags: ["test-noise"],
20005
- requires: ["nextjs"],
20006
- severity: "warn",
20007
- recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
20008
- create: (context) => ({ JSXOpeningElement(node) {
20009
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
20010
- const attributes = node.attributes ?? [];
20011
- const downloadAttribute = findJsxAttribute(attributes, "download");
20012
- if (downloadAttribute) {
20013
- if (!downloadAttribute.value) return;
20014
- const downloadValue = getJsxAttributeLiteralValue(downloadAttribute.value);
20015
- if (downloadValue !== false && downloadValue !== null) return;
20016
- }
20017
- if (getJsxAttributeLiteralValue(findJsxAttribute(attributes, "target")?.value) === "_blank") return;
20018
- const hrefAttribute = findJsxAttribute(attributes, "href");
20019
- if (!hrefAttribute?.value) return;
20020
- const hrefValue = getJsxAttributeLiteralValue(hrefAttribute.value);
20021
- if (typeof hrefValue === "string" && hrefValue.startsWith("/") && !hrefValue.startsWith("//")) context.report({
20022
- node,
20023
- message: "Plain <a> reloads the whole page for internal links, so Next.js loses client-side navigation and prefetching."
20024
- });
20025
- } })
20026
- });
20027
19693
  //#endregion
20028
19694
  //#region src/plugin/utils/contains-fetch-call.ts
20029
19695
  const isFetchCall$1 = (node) => {
@@ -25151,7 +24817,7 @@ const isAsyncFunctionLike = (node) => {
25151
24817
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
25152
24818
  return false;
25153
24819
  };
25154
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
24820
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
25155
24821
  "forEach",
25156
24822
  "map",
25157
24823
  "filter",
@@ -25172,7 +24838,7 @@ const runsOnEffectDispatch = (functionNode) => {
25172
24838
  if (parent.callee === functionNode) return true;
25173
24839
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
25174
24840
  const callee = parent.callee;
25175
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
24841
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
25176
24842
  };
25177
24843
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
25178
24844
  const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
@@ -27805,14 +27471,14 @@ const hasDocumentClassListMutation = (node) => {
27805
27471
  //#endregion
27806
27472
  //#region src/plugin/rules/state-and-effects/no-effect-event-handler.ts
27807
27473
  const hasEventLikeNode = (node) => findTriggeredSideEffectCalleeName(node) !== null || hasDocumentClassListMutation(node);
27808
- const unwrapChainExpression$2 = (node) => {
27474
+ const unwrapChainExpression$1 = (node) => {
27809
27475
  if (!node) return null;
27810
27476
  if (isNodeOfType(node, "ChainExpression")) return node.expression;
27811
27477
  return node;
27812
27478
  };
27813
27479
  const collectGuardExpressions = (node, into) => {
27814
27480
  if (!node) return;
27815
- const unwrappedNode = unwrapChainExpression$2(node);
27481
+ const unwrappedNode = unwrapChainExpression$1(node);
27816
27482
  if (!unwrappedNode) return;
27817
27483
  const rootIdentifierName = getRootIdentifierName(unwrappedNode);
27818
27484
  if (rootIdentifierName) {
@@ -27843,7 +27509,7 @@ const isReturnOnlyStatement = (node) => {
27843
27509
  };
27844
27510
  const hasEventLikeRemainingStatements = (statements) => statements.some((statement) => !isNodeOfType(statement, "ReturnStatement") && hasEventLikeNode(statement));
27845
27511
  const doesGuardMatchDependency = (guardExpression, dependencyExpression) => {
27846
- const unwrappedDependencyExpression = unwrapChainExpression$2(dependencyExpression);
27512
+ const unwrappedDependencyExpression = unwrapChainExpression$1(dependencyExpression);
27847
27513
  if (!unwrappedDependencyExpression) return false;
27848
27514
  if (areExpressionsStructurallyEqual(guardExpression.expression, unwrappedDependencyExpression)) return true;
27849
27515
  return isNodeOfType(unwrappedDependencyExpression, "Identifier") && unwrappedDependencyExpression.name === guardExpression.rootIdentifierName;
@@ -29902,6 +29568,57 @@ const noGrayOnColoredBackground = defineRule({
29902
29568
  } })
29903
29569
  });
29904
29570
  //#endregion
29571
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29572
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29573
+ let ancestor = bindingIdentifier.parent;
29574
+ while (ancestor) {
29575
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29576
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29577
+ ancestor = ancestor.parent ?? null;
29578
+ }
29579
+ return null;
29580
+ };
29581
+ //#endregion
29582
+ //#region src/plugin/utils/executes-during-render.ts
29583
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29584
+ "map",
29585
+ "filter",
29586
+ "forEach",
29587
+ "flatMap",
29588
+ "reduce",
29589
+ "reduceRight",
29590
+ "some",
29591
+ "every",
29592
+ "find",
29593
+ "findIndex",
29594
+ "findLast",
29595
+ "findLastIndex",
29596
+ "sort",
29597
+ "toSorted"
29598
+ ]);
29599
+ const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
29600
+ const isGlobalArrayFromMember = (node, scopes) => {
29601
+ const unwrappedNode = stripParenExpression(node);
29602
+ if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
29603
+ return unwrappedNode.property.name === "from";
29604
+ };
29605
+ const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
29606
+ const unwrappedNode = stripParenExpression(node);
29607
+ if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
29608
+ const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
29609
+ if (!binding?.initializer) return false;
29610
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29611
+ return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
29612
+ };
29613
+ const executesDuringRender = (functionNode, scopes) => {
29614
+ const parent = functionNode.parent;
29615
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29616
+ if (parent.callee === functionNode) return true;
29617
+ if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
29618
+ if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
29619
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
29620
+ };
29621
+ //#endregion
29905
29622
  //#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
29906
29623
  const findEnclosingJsxOpeningElement = (node) => {
29907
29624
  let cursor = node.parent;
@@ -29913,6 +29630,17 @@ const findEnclosingJsxOpeningElement = (node) => {
29913
29630
  return null;
29914
29631
  };
29915
29632
  //#endregion
29633
+ //#region src/plugin/utils/find-render-phase-component-or-hook.ts
29634
+ const findRenderPhaseComponentOrHook = (node, scopes) => {
29635
+ let functionNode = findEnclosingFunction(node);
29636
+ while (functionNode) {
29637
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29638
+ if (!executesDuringRender(functionNode, scopes)) return null;
29639
+ functionNode = findEnclosingFunction(functionNode);
29640
+ }
29641
+ return null;
29642
+ };
29643
+ //#endregion
29916
29644
  //#region src/plugin/utils/has-client-render-evidence.ts
29917
29645
  const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29918
29646
  if (fileHasUseClientDirective) return true;
@@ -30021,6 +29749,9 @@ const isGatedByFalsyInitialState = (node, scopes) => {
30021
29749
  return false;
30022
29750
  };
30023
29751
  //#endregion
29752
+ //#region src/plugin/utils/is-event-handler-attribute.ts
29753
+ const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
29754
+ //#endregion
30024
29755
  //#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
30025
29756
  const evaluateEquality = (operator, left, right) => {
30026
29757
  if (operator === "===" || operator === "==") return left === right;
@@ -32119,41 +31850,6 @@ const noMutableInDeps = defineRule({
32119
31850
  }
32120
31851
  });
32121
31852
  //#endregion
32122
- //#region src/plugin/utils/is-result-discarded-call.ts
32123
- const isResultDiscardedCall = (callExpression) => {
32124
- let node = callExpression;
32125
- let parent = node.parent;
32126
- while (parent) {
32127
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
32128
- if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
32129
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
32130
- if (isNodeOfType(parent, "ChainExpression")) {
32131
- node = parent;
32132
- parent = node.parent;
32133
- continue;
32134
- }
32135
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
32136
- node = parent;
32137
- parent = node.parent;
32138
- continue;
32139
- }
32140
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
32141
- node = parent;
32142
- parent = node.parent;
32143
- continue;
32144
- }
32145
- if (isNodeOfType(parent, "SequenceExpression")) {
32146
- const expressions = parent.expressions ?? [];
32147
- if (expressions[expressions.length - 1] !== node) return true;
32148
- node = parent;
32149
- parent = node.parent;
32150
- continue;
32151
- }
32152
- return false;
32153
- }
32154
- return false;
32155
- };
32156
- //#endregion
32157
31853
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
32158
31854
  const LODASH_MUTATOR_NAMES = new Set([
32159
31855
  "set",
@@ -33444,7 +33140,7 @@ const isUseRefIdentifier = (identifier) => {
33444
33140
  };
33445
33141
  const COMMAND_PROP_NAME_PATTERN = /^(fetch|load|refetch|dispatch|register|render)([A-Z_]|$)/;
33446
33142
  const SETTER_NAMED_PROP_PATTERN = /^set[A-Z]/;
33447
- const unwrapChainExpression$1 = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
33143
+ const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
33448
33144
  const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
33449
33145
  "useCallback",
33450
33146
  "useMemo",
@@ -33479,7 +33175,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
33479
33175
  return Boolean(ref.resolved?.defs.some((def) => {
33480
33176
  const node = def.node;
33481
33177
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
33482
- const initializer = unwrapChainExpression$1(node.init);
33178
+ const initializer = unwrapChainExpression(node.init);
33483
33179
  const wrappedFunction = getWrapperHookWrappedFunction(initializer);
33484
33180
  if (wrappedFunction) {
33485
33181
  if (wrappedFunction.async) return false;
@@ -33492,7 +33188,7 @@ const isDirectParentCallbackRef = (analysis, ref) => {
33492
33188
  const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
33493
33189
  const node = def.node;
33494
33190
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
33495
- return getWrapperHookWrappedFunction(unwrapChainExpression$1(node.init)) !== null;
33191
+ return getWrapperHookWrappedFunction(unwrapChainExpression(node.init)) !== null;
33496
33192
  }));
33497
33193
  const isHandlerBagArgument = (analysis, argument) => {
33498
33194
  if (!isNodeOfType(argument, "ObjectExpression")) return false;
@@ -33514,7 +33210,7 @@ const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
33514
33210
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
33515
33211
  const node = def.node;
33516
33212
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
33517
- const init = unwrapChainExpression$1(node.init);
33213
+ const init = unwrapChainExpression(node.init);
33518
33214
  if (!isNodeOfType(init, "CallExpression")) return false;
33519
33215
  const callee = init.callee;
33520
33216
  if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
@@ -33558,7 +33254,7 @@ const noPassDataToParent = defineRule({
33558
33254
  if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
33559
33255
  if (isRefCall(analysis, ref)) continue;
33560
33256
  if (!isSynchronous(ref.identifier, effectFn)) continue;
33561
- const calleeNode = unwrapChainExpression$1(callExpr.callee);
33257
+ const calleeNode = unwrapChainExpression(callExpr.callee);
33562
33258
  const identifier = ref.identifier;
33563
33259
  if (calleeNode === identifier) {
33564
33260
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
@@ -40846,123 +40542,6 @@ const preferUseEffectEvent = defineRule({
40846
40542
  }
40847
40543
  });
40848
40544
  //#endregion
40849
- //#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
40850
- const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
40851
- "each",
40852
- "every",
40853
- "filter",
40854
- "find",
40855
- "findIndex",
40856
- "findLast",
40857
- "findLastIndex",
40858
- "flatMap",
40859
- "forEach",
40860
- "map",
40861
- "reduce",
40862
- "reduceRight",
40863
- "some",
40864
- "sort",
40865
- "toSorted"
40866
- ]);
40867
- const STATIC_ITERATOR_CALLBACK_METHOD_NAMES = new Set([
40868
- "from",
40869
- "fromAsync",
40870
- "groupBy"
40871
- ]);
40872
- const unwrapChainExpression = (node) => isNodeOfType(node, "ChainExpression") ? node.expression : node;
40873
- const isNullLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null;
40874
- const isListenerRemovalViaNullHandler = (callNode) => {
40875
- if (!isNodeOfType(callNode, "CallExpression")) return false;
40876
- const callee = unwrapChainExpression(callNode.callee);
40877
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "on" && isNullLiteral(callNode.arguments?.[1]);
40878
- };
40879
- const REFERENCE_BASED_REMOVAL_METHOD_NAMES = new Set([
40880
- "off",
40881
- "removeEventListener",
40882
- "removeListener"
40883
- ]);
40884
- const isNoOpInlineHandlerRemoval = (callNode, methodName) => {
40885
- if (!REFERENCE_BASED_REMOVAL_METHOD_NAMES.has(methodName)) return false;
40886
- const handlerArgument = callNode.arguments?.[1];
40887
- return isNodeOfType(handlerArgument, "ArrowFunctionExpression") || isNodeOfType(handlerArgument, "FunctionExpression");
40888
- };
40889
- const isReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
40890
- const callNode = unwrapChainExpression(node);
40891
- if (!isNodeOfType(callNode, "CallExpression")) return false;
40892
- if (isListenerRemovalViaNullHandler(callNode)) return true;
40893
- const callee = unwrapChainExpression(callNode.callee);
40894
- if (isNodeOfType(callee, "Identifier")) {
40895
- if (TIMER_CLEANUP_CALLEE_NAMES.has(callee.name)) return true;
40896
- if (CLEANUP_LIKE_RELEASE_CALLEE_NAMES.has(callee.name)) return true;
40897
- if (knownCleanupFunctionNames.has(callee.name)) return true;
40898
- return false;
40899
- }
40900
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
40901
- if (isNoOpInlineHandlerRemoval(callNode, callee.property.name)) return false;
40902
- if (BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier") && knownBoundSubscriptionNames.has(callee.object.name)) return true;
40903
- return GLOBAL_RELEASE_METHOD_NAMES.has(callee.property.name);
40904
- }
40905
- return false;
40906
- };
40907
- 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);
40908
- const isIteratorCallbackArgument = (node) => {
40909
- const parentNode = node.parent;
40910
- if (!isNodeOfType(parentNode, "CallExpression")) return false;
40911
- if (!parentNode.arguments?.some((argument) => argument === node)) return false;
40912
- const callee = unwrapChainExpression(parentNode.callee);
40913
- if (parentNode.arguments[1] === node && isStaticIteratorCallbackCallee(callee)) return true;
40914
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_CALLBACK_METHOD_NAMES.has(callee.property.name);
40915
- };
40916
- const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
40917
- let didFindRelease = false;
40918
- walkAst(node, (child) => {
40919
- if (didFindRelease) return false;
40920
- if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
40921
- if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
40922
- didFindRelease = true;
40923
- return false;
40924
- }
40925
- });
40926
- return didFindRelease;
40927
- };
40928
- const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
40929
- if (!isFunctionLike$1(node)) return false;
40930
- return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
40931
- };
40932
- const isProvablyNoOpCleanupFunction = (node) => {
40933
- if (!isFunctionLike$1(node)) return false;
40934
- let sawNoOpRemoval = false;
40935
- let sawOtherCall = false;
40936
- walkAst(node.body, (child) => {
40937
- if (sawOtherCall) return false;
40938
- if (isFunctionLike$1(child)) return false;
40939
- const callNode = unwrapChainExpression(child);
40940
- if (!isNodeOfType(callNode, "CallExpression")) return;
40941
- const callee = unwrapChainExpression(callNode.callee);
40942
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isNoOpInlineHandlerRemoval(callNode, callee.property.name)) {
40943
- sawNoOpRemoval = true;
40944
- return;
40945
- }
40946
- sawOtherCall = true;
40947
- });
40948
- return sawNoOpRemoval && !sawOtherCall;
40949
- };
40950
- const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
40951
- if (!returnedValue) return false;
40952
- const unwrappedValue = unwrapChainExpression(returnedValue);
40953
- if (isNodeOfType(unwrappedValue, "Literal") && unwrappedValue.value === null) return false;
40954
- if (isNodeOfType(unwrappedValue, "Identifier")) {
40955
- if (unwrappedValue.name === "undefined") return false;
40956
- if (knownCleanupFunctionNames.has(unwrappedValue.name)) return true;
40957
- return options.allowOpaqueReturn === true && !knownBoundSubscriptionNames.has(unwrappedValue.name);
40958
- }
40959
- if (isCleanupReturningSubscribeLikeCallExpression(unwrappedValue)) return true;
40960
- if (isProvablyNoOpCleanupFunction(unwrappedValue)) return false;
40961
- if (options.allowOpaqueReturn === true && !isSubscribeLikeCallExpression(unwrappedValue)) return true;
40962
- if (isCleanupFunctionLike(unwrappedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames)) return true;
40963
- return false;
40964
- };
40965
- //#endregion
40966
40545
  //#region src/plugin/rules/state-and-effects/prefer-use-sync-external-store.ts
40967
40546
  const findUseEffectsInComponent = (componentBody) => {
40968
40547
  const effectCalls = [];