oxlint-plugin-react-doctor 0.7.7-dev.287dff7 → 0.7.7-dev.4190897

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 +1276 -93
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4573,6 +4573,23 @@ const hasPossibleStaticPropertyWrite = (identifier, propertyName, scopes) => {
4573
4573
  return writtenPropertyName === null || writtenPropertyName === propertyName;
4574
4574
  }));
4575
4575
  };
4576
+ const canExecuteBefore = (candidateNode, referenceNode, scopes) => {
4577
+ const candidateBoundary = findExecutionBoundary(candidateNode);
4578
+ const referenceBoundary = findExecutionBoundary(referenceNode);
4579
+ if (!candidateBoundary || !referenceBoundary) return true;
4580
+ if (candidateBoundary === referenceBoundary) return candidateNode.range[0] < referenceNode.range[0];
4581
+ if (!isFunctionLike$1(candidateBoundary)) return true;
4582
+ return isFunctionSynchronouslyInvokedBefore(candidateBoundary, referenceNode, scopes);
4583
+ };
4584
+ const hasPossibleStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, scopes) => {
4585
+ if (!isNodeOfType(identifier, "Identifier")) return false;
4586
+ return getPotentiallyAliasedSymbols(identifier, scopes).some((symbol) => symbol.references.some((reference) => {
4587
+ const writeTarget = getMemberWriteTarget(reference.identifier);
4588
+ if (!writeTarget || !canExecuteBefore(writeTarget, referenceNode, scopes)) return false;
4589
+ const writtenPropertyName = getResolvedStaticPropertyName(writeTarget, scopes);
4590
+ return writtenPropertyName === null || writtenPropertyName === propertyName;
4591
+ }));
4592
+ };
4576
4593
  const hasPossibleStaticPropertyMutationOrEscape = (identifier, propertyName, scopes) => {
4577
4594
  if (!isNodeOfType(identifier, "Identifier")) return false;
4578
4595
  if (hasPossibleStaticPropertyWrite(identifier, propertyName, scopes)) return true;
@@ -11673,6 +11690,25 @@ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(
11673
11690
  //#region src/plugin/utils/is-event-handler-attribute.ts
11674
11691
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
11675
11692
  //#endregion
11693
+ //#region src/plugin/utils/is-ast-descendant.ts
11694
+ /**
11695
+ * True when `inner` is `outer` itself or any descendant in the AST
11696
+ * `parent` chain. Walks `inner.parent` upward and stops at either a
11697
+ * match (`true`) or the chain's root (`false`).
11698
+ *
11699
+ * Was duplicated byte-identical in:
11700
+ * - exhaustive-deps-symbol-stability.ts
11701
+ * - semantic/closure-captures.ts
11702
+ */
11703
+ const isAstDescendant = (inner, outer) => {
11704
+ let current = inner;
11705
+ while (current) {
11706
+ if (current === outer) return true;
11707
+ current = current.parent ?? null;
11708
+ }
11709
+ return false;
11710
+ };
11711
+ //#endregion
11676
11712
  //#region src/plugin/utils/react-ref-origin.ts
11677
11713
  const resolveReactRefSymbol = (memberExpression, scopes) => {
11678
11714
  const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
@@ -12322,26 +12358,39 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
12322
12358
  });
12323
12359
  return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
12324
12360
  };
12361
+ const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => {
12362
+ if (usage.handleKey === null) return null;
12363
+ const doesTestRequireLiveHandle = (test) => {
12364
+ if (resolveExpressionKey(test, context) === usage.handleKey) return true;
12365
+ const unwrappedTest = stripParenExpression(test);
12366
+ if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
12367
+ const isNullishOperand = (operand) => {
12368
+ const unwrappedOperand = stripParenExpression(operand);
12369
+ return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
12370
+ };
12371
+ return resolveExpressionKey(unwrappedTest.left, context) === usage.handleKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === usage.handleKey && isNullishOperand(unwrappedTest.left);
12372
+ };
12373
+ let ancestor = releaseCall.parent;
12374
+ while (ancestor && ancestor !== owner) {
12375
+ if (isNodeOfType(ancestor, "IfStatement")) {
12376
+ if (ancestor.alternate !== null || !doesTestRequireLiveHandle(ancestor.test) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
12377
+ return ancestor;
12378
+ }
12379
+ ancestor = ancestor.parent;
12380
+ }
12381
+ return null;
12382
+ };
12325
12383
  const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
12326
12384
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
12327
12385
  const functionCfg = context.cfg.cfgFor(callback);
12328
12386
  const usageBlock = functionCfg?.blockOf(usage.node);
12329
12387
  const usageStart = getRangeStart(usage.node);
12330
12388
  if (!functionCfg || !usageBlock || usageStart === null) return false;
12331
- const findHandleGuard = (releaseCall) => {
12332
- if (usage.handleKey === null) return null;
12333
- let ancestor = releaseCall.parent;
12334
- while (ancestor && ancestor !== callback.body) {
12335
- if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
12336
- ancestor = ancestor.parent;
12337
- }
12338
- return null;
12339
- };
12340
12389
  const matchingReleaseAnchors = [];
12341
12390
  walkInsideStatementBlocks(callback.body, (child) => {
12342
12391
  if (!isNodeOfType(child, "CallExpression")) return;
12343
12392
  const releaseStart = getRangeStart(child);
12344
- const handleGuard = findHandleGuard(child);
12393
+ const handleGuard = findDirectHandleGuardForRelease(child, callback, usage, context);
12345
12394
  if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
12346
12395
  if (doesReleaseCallMatchUsage(child, usage, context)) {
12347
12396
  matchingReleaseAnchors.push(handleGuard ?? child);
@@ -12371,12 +12420,12 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
12371
12420
  return didFindUnmountCleanup;
12372
12421
  };
12373
12422
  const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
12374
- const collectBlockingBooleanStates = (expression, blockedExpressionValue, context) => {
12423
+ const collectBlockingBooleanStates = (expression, blockedExpressionValue, guardNode, context) => {
12375
12424
  const unwrappedExpression = stripParenExpression(expression);
12376
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, context);
12425
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, guardNode, context);
12377
12426
  if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
12378
12427
  if (!(unwrappedExpression.operator === "||" && blockedExpressionValue || unwrappedExpression.operator === "&&" && !blockedExpressionValue)) return [];
12379
- return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, context)];
12428
+ return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, guardNode, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, guardNode, context)];
12380
12429
  }
12381
12430
  if (isNodeOfType(unwrappedExpression, "BinaryExpression") && [
12382
12431
  "===",
@@ -12387,35 +12436,59 @@ const collectBlockingBooleanStates = (expression, blockedExpressionValue, contex
12387
12436
  const leftValue = readStaticBoolean(unwrappedExpression.left);
12388
12437
  const rightValue = readStaticBoolean(unwrappedExpression.right);
12389
12438
  const booleanValue = leftValue ?? rightValue;
12390
- const comparedKey = resolveExpressionKey(leftValue === null ? unwrappedExpression.left : unwrappedExpression.right, context);
12439
+ const comparedExpression = leftValue === null ? unwrappedExpression.left : unwrappedExpression.right;
12440
+ const comparedKey = resolveExpressionKey(comparedExpression, context);
12391
12441
  if (booleanValue === null || comparedKey === null) return [];
12442
+ const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
12392
12443
  return [{
12444
+ bindingIdentifier: isNodeOfType(comparedExpression, "Identifier") ? context.scopes.symbolFor(comparedExpression)?.bindingIdentifier ?? null : null,
12445
+ guardNode,
12393
12446
  key: comparedKey,
12394
- value: (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==") === blockedExpressionValue ? booleanValue : !booleanValue
12447
+ value: isEquality === blockedExpressionValue ? booleanValue : !booleanValue
12395
12448
  }];
12396
12449
  }
12397
12450
  const expressionKey = resolveExpressionKey(unwrappedExpression, context);
12398
12451
  return expressionKey === null ? [] : [{
12452
+ bindingIdentifier: isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression)?.bindingIdentifier ?? null : null,
12453
+ guardNode,
12399
12454
  key: expressionKey,
12400
12455
  value: blockedExpressionValue
12401
12456
  }];
12402
12457
  };
12403
- const isDirectEarlyReturnConsequent = (ifStatement) => {
12404
- if (!isNodeOfType(ifStatement, "IfStatement") || ifStatement.alternate) return false;
12405
- if (isNodeOfType(ifStatement.consequent, "ReturnStatement")) return true;
12406
- return isNodeOfType(ifStatement.consequent, "BlockStatement") && ifStatement.consequent.body.length === 1 && isNodeOfType(ifStatement.consequent.body[0], "ReturnStatement");
12458
+ const canNodeReachLaterNodeWithinFunction = (sourceNode, targetNode, owner, context) => {
12459
+ const functionCfg = context.cfg.cfgFor(owner);
12460
+ const sourceBlock = functionCfg?.blockOf(sourceNode);
12461
+ const targetBlock = functionCfg?.blockOf(targetNode);
12462
+ const sourceStart = getRangeStart(sourceNode);
12463
+ const targetStart = getRangeStart(targetNode);
12464
+ if (!functionCfg || !sourceBlock || !targetBlock || sourceStart === null || targetStart === null) return true;
12465
+ if (!isNodeReachableWithinFunction(sourceNode, context)) return false;
12466
+ if (sourceBlock === targetBlock) return sourceStart < targetStart;
12467
+ const visitedBlocks = new Set([sourceBlock]);
12468
+ const pendingBlocks = [sourceBlock];
12469
+ while (pendingBlocks.length > 0) {
12470
+ const currentBlock = pendingBlocks.pop();
12471
+ if (!currentBlock) break;
12472
+ for (const edge of currentBlock.successors) {
12473
+ if (edge.to === targetBlock) return true;
12474
+ if (visitedBlocks.has(edge.to)) continue;
12475
+ visitedBlocks.add(edge.to);
12476
+ pendingBlocks.push(edge.to);
12477
+ }
12478
+ }
12479
+ return false;
12407
12480
  };
12408
12481
  const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
12409
12482
  if (!isFunctionLike$1(callback) || callback.async) return [];
12410
12483
  const guardStates = [];
12411
12484
  walkAst(callback.body, (child) => {
12412
12485
  if (child !== callback.body && isFunctionLike$1(child)) return false;
12413
- if (isNodeOfType(child, "IfStatement") && isDirectEarlyReturnConsequent(child) && doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], callback, context)) guardStates.push(...collectBlockingBooleanStates(child.test, true, context));
12486
+ if (isNodeOfType(child, "IfStatement") && !child.alternate && !canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, callback, context) && doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], callback, context)) guardStates.push(...collectBlockingBooleanStates(child.test, true, child, context));
12414
12487
  });
12415
12488
  let descendant = usageNode;
12416
12489
  let ancestor = descendant.parent;
12417
12490
  while (ancestor && ancestor !== callback) {
12418
- if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, context));
12491
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, ancestor, context));
12419
12492
  descendant = ancestor;
12420
12493
  ancestor = ancestor.parent;
12421
12494
  }
@@ -12424,7 +12497,7 @@ const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
12424
12497
  const cleanupReturnInvalidatesGuard = (cleanupReturn, guardState, context) => {
12425
12498
  if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
12426
12499
  const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
12427
- if (!cleanupFunction || !isFunctionLike$1(cleanupFunction) || cleanupFunction.async) return false;
12500
+ if (!cleanupFunction || !isFunctionLike$1(cleanupFunction) || cleanupFunction.async || cleanupFunction.generator) return false;
12428
12501
  let didInvalidateGuard = false;
12429
12502
  walkAst(cleanupFunction.body, (child) => {
12430
12503
  if (didInvalidateGuard) return false;
@@ -12453,11 +12526,110 @@ const deferredUsageWritesGuardBeforeUsage = (callback, usageNode, guardState, co
12453
12526
  });
12454
12527
  return didWriteGuard;
12455
12528
  };
12529
+ const canInterruptionReachUsageThroughCatch = (interruptionNode, usageNode, owner, context) => {
12530
+ let descendant = interruptionNode;
12531
+ let ancestor = descendant.parent;
12532
+ while (ancestor && ancestor !== owner) {
12533
+ if (isNodeOfType(ancestor, "TryStatement") && ancestor.block === descendant && ancestor.handler && canNodeReachLaterNodeWithinFunction(ancestor.handler.body, usageNode, owner, context)) return true;
12534
+ descendant = ancestor;
12535
+ ancestor = ancestor.parent;
12536
+ }
12537
+ return false;
12538
+ };
12539
+ const isEffectLocalLifecycleGuard = (callback, guardState, cleanupFunctions, context) => {
12540
+ if (!guardState.bindingIdentifier) return false;
12541
+ const guardSymbol = context.scopes.symbolFor(guardState.bindingIdentifier);
12542
+ if (!guardSymbol || guardSymbol.kind !== "let" && guardSymbol.kind !== "var" || !isNodeOfType(guardSymbol.declarationNode, "VariableDeclarator") || findEnclosingFunction$1(guardSymbol.declarationNode) !== callback) return false;
12543
+ return guardSymbol.references.every((reference) => {
12544
+ if (!isWithinAssignmentTarget(reference.identifier)) return true;
12545
+ const assignmentTarget = findTransparentExpressionRoot(reference.identifier);
12546
+ const assignment = assignmentTarget.parent;
12547
+ return isNodeOfType(assignment, "AssignmentExpression") && assignment.operator === "=" && assignment.left === assignmentTarget && readStaticBoolean(assignment.right) === guardState.value && cleanupFunctions.includes(findEnclosingFunction$1(assignment) ?? assignment);
12548
+ });
12549
+ };
12550
+ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, context) => {
12551
+ if (!isFunctionLike$1(callback)) return true;
12552
+ const guardStart = getRangeStart(guardState.guardNode);
12553
+ const usageStart = getRangeStart(usageNode);
12554
+ if (guardStart === null || usageStart === null) return true;
12555
+ let hasPotentialInterruption = false;
12556
+ walkAst(callback.body, (child) => {
12557
+ if (hasPotentialInterruption) return false;
12558
+ if (child !== callback.body && isFunctionLike$1(child)) return false;
12559
+ const childStart = getRangeStart(child);
12560
+ if (childStart === null || childStart <= guardStart || childStart >= usageStart) return;
12561
+ if (isNodeOfType(child, "CallExpression") || isNodeOfType(child, "AwaitExpression") || isNodeOfType(child, "YieldExpression")) {
12562
+ if (canNodeReachLaterNodeWithinFunction(child, usageNode, callback, context) || canInterruptionReachUsageThroughCatch(child, usageNode, callback, context)) {
12563
+ hasPotentialInterruption = true;
12564
+ return false;
12565
+ }
12566
+ }
12567
+ });
12568
+ return hasPotentialInterruption;
12569
+ };
12456
12570
  const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
12457
12571
  const usageFunction = findEnclosingFunction$1(usage.node);
12458
12572
  const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
12459
- if (usage.handleKey === null || !usageFunction || usageFunction === callback || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
12460
- return collectDeferredUsageGuardStates(usageFunction, usage.node, context).some((guardState) => !deferredUsageWritesGuardBeforeUsage(usageFunction, usage.node, guardState, context) && cleanupReturns.every((cleanupReturn) => cleanupReturnInvalidatesGuard(cleanupReturn, guardState, context)));
12573
+ if (usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usage.node, "CallExpression") || !isNodeOfType(usage.node.callee, "Identifier") || !context.scopes.isGlobalReference(usage.node.callee) || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
12574
+ const usageExpression = findTransparentExpressionRoot(usage.node);
12575
+ const usageAssignment = usageExpression.parent;
12576
+ if (!isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !isNodeOfType(usageAssignment.left, "Identifier")) return false;
12577
+ const handleSymbol = context.scopes.symbolFor(usageAssignment.left);
12578
+ if (!handleSymbol || handleSymbol.kind !== "let" && handleSymbol.kind !== "var" || !isNodeOfType(handleSymbol.declarationNode, "VariableDeclarator") || findEnclosingFunction$1(handleSymbol.declarationNode) !== callback) return false;
12579
+ const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
12580
+ if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
12581
+ const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
12582
+ return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
12583
+ });
12584
+ if (cleanupFunctions.length !== cleanupReturns.length) return false;
12585
+ const globalReleaseProofsByCleanup = /* @__PURE__ */ new Map();
12586
+ for (const cleanupFunction of cleanupFunctions) {
12587
+ if (!isFunctionLike$1(cleanupFunction)) return false;
12588
+ const globalReleaseProofs = [];
12589
+ walkAst(cleanupFunction.body, (child) => {
12590
+ if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
12591
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && context.scopes.isGlobalReference(child.callee) && doesReleaseCallMatchUsage(child, usage, context)) {
12592
+ const handleGuard = findDirectHandleGuardForRelease(child, cleanupFunction, usage, context);
12593
+ globalReleaseProofs.push({
12594
+ anchor: handleGuard ?? child,
12595
+ call: child,
12596
+ handleGuard
12597
+ });
12598
+ }
12599
+ });
12600
+ if (!doMatchingNodesCoverEveryPathFromFunctionEntry(cleanupFunction, globalReleaseProofs.map((releaseProof) => releaseProof.anchor), context)) return false;
12601
+ globalReleaseProofsByCleanup.set(cleanupFunction, globalReleaseProofs);
12602
+ }
12603
+ const handleAssignments = handleSymbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
12604
+ const hasUsageAssignment = handleAssignments.some((handleAssignment) => findTransparentExpressionRoot(handleAssignment.identifier).parent === usageAssignment);
12605
+ const hasUnsafeHandleAssignment = handleAssignments.some((handleAssignment) => {
12606
+ const assignmentTarget = findTransparentExpressionRoot(handleAssignment.identifier);
12607
+ const assignment = assignmentTarget.parent;
12608
+ if (assignment === usageAssignment) return false;
12609
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== assignmentTarget) return true;
12610
+ const assignedValue = stripParenExpression(assignment.right);
12611
+ if (!(isNodeOfType(assignedValue, "Literal") && assignedValue.value === null || isNodeOfType(assignedValue, "Identifier") && assignedValue.name === "undefined" && context.scopes.isGlobalReference(assignedValue))) return true;
12612
+ const cleanupFunction = findEnclosingFunction$1(assignment);
12613
+ const globalReleaseProofs = cleanupFunction ? globalReleaseProofsByCleanup.get(cleanupFunction) : void 0;
12614
+ return !(cleanupFunction && globalReleaseProofs && doMatchingNodesCoverEveryPathBeforeUsage(assignment, globalReleaseProofs.map((releaseProof) => releaseProof.handleGuard && isAstDescendant(assignment, releaseProof.handleGuard.consequent) ? releaseProof.call : releaseProof.anchor), cleanupFunction, context));
12615
+ });
12616
+ if (!hasUsageAssignment || hasUnsafeHandleAssignment) return false;
12617
+ let usageAncestor = usage.node.parent;
12618
+ while (usageAncestor && usageAncestor !== usageFunction) {
12619
+ if (isNodeOfType(usageAncestor, "ForStatement") || isNodeOfType(usageAncestor, "ForInStatement") || isNodeOfType(usageAncestor, "ForOfStatement") || isNodeOfType(usageAncestor, "WhileStatement") || isNodeOfType(usageAncestor, "DoWhileStatement")) return false;
12620
+ usageAncestor = usageAncestor.parent;
12621
+ }
12622
+ let hasPotentialInterruption = false;
12623
+ for (const argument of usage.node.arguments ?? []) walkAst(argument, (argumentChild) => {
12624
+ if (hasPotentialInterruption) return false;
12625
+ if (isFunctionLike$1(argumentChild)) return false;
12626
+ if (isNodeOfType(argumentChild, "CallExpression") || isNodeOfType(argumentChild, "AwaitExpression") || isNodeOfType(argumentChild, "YieldExpression")) {
12627
+ hasPotentialInterruption = true;
12628
+ return false;
12629
+ }
12630
+ });
12631
+ if (hasPotentialInterruption) return false;
12632
+ return collectDeferredUsageGuardStates(usageFunction, usage.node, context).some((guardState) => isEffectLocalLifecycleGuard(callback, guardState, cleanupFunctions, context) && !hasPotentialInterruptionAfterGuard(usageFunction, guardState, usage.node, context) && !deferredUsageWritesGuardBeforeUsage(usageFunction, usage.node, guardState, context) && cleanupReturns.every((cleanupReturn) => cleanupReturnInvalidatesGuard(cleanupReturn, guardState, context)));
12461
12633
  };
12462
12634
  const effectHasCleanupForUsage = (callback, usage, context) => {
12463
12635
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
@@ -13023,7 +13195,7 @@ const effectNeedsCleanup = defineRule({
13023
13195
  const hookName = getCalleeName$2(node) ?? "effect";
13024
13196
  context.report({
13025
13197
  node,
13026
- message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
13198
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without guaranteed cleanup. Return a cleanup function that owns every allocation so it does not leak after unmount.`
13027
13199
  });
13028
13200
  },
13029
13201
  FunctionDeclaration(node) {
@@ -14454,7 +14626,7 @@ const buildAsyncEffectMessage = (hookName) => `\`${hookName}\` was given an asyn
14454
14626
  const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s callback is defined elsewhere, so dependencies can't be checked and stale values can slip through.`;
14455
14627
  const buildUnstableDepMessage = (hookName, depName) => `\`${depName}\` is rebuilt every render, so \`${hookName}\` runs every time.`;
14456
14628
  const buildForwardedUnstableDepMessage = (depName) => `\`${depName}\` is rebuilt every render and reaches a Hook dependency inside this custom Hook.`;
14457
- const buildSetStateWithoutDepsMessage = (hookName, setterName) => `\`${hookName}\` calls \`${setterName}\` with no dependency array, so it can loop forever & freeze the component.`;
14629
+ const buildSetStateWithoutDepsMessage = (hookName, setterName) => `\`${hookName}\` calls \`${setterName}\` with a value that can change on every render and no dependency array, so it can keep triggering renders.`;
14458
14630
  const buildRefCleanupMessage = (depName) => `Your cleanup may read the wrong node since the ref \`${depName}\` can change before it runs.`;
14459
14631
  const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render, so the next render reads the old value.`;
14460
14632
  //#endregion
@@ -14473,25 +14645,6 @@ const resolveExhaustiveDepsSettings = (settings) => {
14473
14645
  };
14474
14646
  };
14475
14647
  //#endregion
14476
- //#region src/plugin/utils/is-ast-descendant.ts
14477
- /**
14478
- * True when `inner` is `outer` itself or any descendant in the AST
14479
- * `parent` chain. Walks `inner.parent` upward and stops at either a
14480
- * match (`true`) or the chain's root (`false`).
14481
- *
14482
- * Was duplicated byte-identical in:
14483
- * - exhaustive-deps-symbol-stability.ts
14484
- * - semantic/closure-captures.ts
14485
- */
14486
- const isAstDescendant = (inner, outer) => {
14487
- let current = inner;
14488
- while (current) {
14489
- if (current === outer) return true;
14490
- current = current.parent ?? null;
14491
- }
14492
- return false;
14493
- };
14494
- //#endregion
14495
14648
  //#region src/plugin/rules/react-builtins/exhaustive-deps-sole-writer-guard.ts
14496
14649
  const EQUALITY_BINARY_OPERATORS = new Set(["===", "!=="]);
14497
14650
  const getUseStateSetterSymbol = (stateSymbol, scopes) => {
@@ -15233,26 +15386,125 @@ const isExtraDepAllowedForHook = (hookName, node, scopes) => {
15233
15386
  };
15234
15387
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
15235
15388
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
15236
- const isStableSetterLikeSymbol = (symbol, scopes) => {
15237
- if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
15238
- return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
15389
+ const FRESH_STATE_CONSTRUCTOR_NAMES = new Set([
15390
+ "Array",
15391
+ "Date",
15392
+ "Error",
15393
+ "Map",
15394
+ "Object",
15395
+ "RegExp",
15396
+ "Set",
15397
+ "WeakMap",
15398
+ "WeakSet"
15399
+ ]);
15400
+ const getBindingIdentifier = (node) => {
15401
+ if (!node) return null;
15402
+ const candidate = isNodeOfType(node, "AssignmentPattern") ? node.left : node;
15403
+ return isNodeOfType(candidate, "Identifier") ? candidate : null;
15239
15404
  };
15240
- const isConvergingFunctionalUpdater = (node, scopes) => {
15241
- const updater = unwrapExpression$3(node);
15242
- if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
15243
- const previousValueParameter = updater.params?.[0];
15244
- if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
15245
- let returnedExpression = updater.body;
15246
- if (isNodeOfType(updater.body, "BlockStatement")) {
15247
- returnedExpression = null;
15248
- for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
15249
- returnedExpression = statement.argument;
15250
- break;
15405
+ const resolveStateSetterSymbol = (identifier, scopes) => {
15406
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
15407
+ let symbol = scopes.symbolFor(identifier);
15408
+ while (symbol?.kind === "const" && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && symbol.declarationNode.id === symbol.bindingIdentifier && symbol.initializer) {
15409
+ if (visitedSymbolIds.has(symbol.id)) return null;
15410
+ visitedSymbolIds.add(symbol.id);
15411
+ const initializer = unwrapExpression$3(symbol.initializer);
15412
+ if (!isNodeOfType(initializer, "Identifier")) return symbol;
15413
+ symbol = scopes.symbolFor(initializer);
15414
+ }
15415
+ return symbol;
15416
+ };
15417
+ const getStateSetterDescriptor = (identifier, scopes) => {
15418
+ const setterSymbol = resolveStateSetterSymbol(identifier, scopes);
15419
+ if (!setterSymbol || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
15420
+ const declarator = setterSymbol.declarationNode;
15421
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
15422
+ const setterElement = declarator.id.elements?.[1];
15423
+ const setterBinding = isAstNode(setterElement) ? getBindingIdentifier(setterElement) : null;
15424
+ if (!setterBinding || setterBinding !== setterSymbol.bindingIdentifier) return null;
15425
+ const initializer = declarator.init ? unwrapExpression$3(declarator.init) : null;
15426
+ if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useState", scopes, {
15427
+ allowGlobalReactNamespace: true,
15428
+ allowUnboundBareCalls: true,
15429
+ resolveNamedAliases: true
15430
+ })) return null;
15431
+ const stateElement = declarator.id.elements?.[0];
15432
+ const stateBinding = isAstNode(stateElement) ? getBindingIdentifier(stateElement) : null;
15433
+ return {
15434
+ setterSymbol,
15435
+ stateSymbol: stateBinding ? scopes.symbolFor(stateBinding) : null
15436
+ };
15437
+ };
15438
+ const expressionReadsSymbol = (node, symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
15439
+ if (!symbol) return false;
15440
+ const candidate = unwrapExpression$3(node);
15441
+ if (isNodeOfType(candidate, "Identifier")) {
15442
+ const reference = scopes.referenceFor(candidate);
15443
+ if (reference?.flag !== "write" && reference?.resolvedSymbol?.id === symbol.id) return true;
15444
+ const candidateSymbol = reference?.resolvedSymbol;
15445
+ if (candidateSymbol?.kind === "const" && candidateSymbol.initializer && !isOutsideAllFunctions(candidateSymbol) && !visitedSymbolIds.has(candidateSymbol.id)) {
15446
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
15447
+ nextVisitedSymbolIds.add(candidateSymbol.id);
15448
+ return expressionReadsSymbol(candidateSymbol.initializer, symbol, scopes, nextVisitedSymbolIds);
15251
15449
  }
15450
+ return false;
15451
+ }
15452
+ const record = candidate;
15453
+ for (const key of Object.keys(record)) {
15454
+ if (key === "parent") continue;
15455
+ const child = record[key];
15456
+ if (Array.isArray(child)) {
15457
+ if (child.some((item) => isAstNode(item) && expressionReadsSymbol(item, symbol, scopes))) return true;
15458
+ } else if (isAstNode(child) && expressionReadsSymbol(child, symbol, scopes)) return true;
15459
+ }
15460
+ return false;
15461
+ };
15462
+ const isGuaranteedFreshStateValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
15463
+ const candidate = unwrapExpression$3(node);
15464
+ if (isNodeOfType(candidate, "ObjectExpression") || isNodeOfType(candidate, "ArrayExpression") || isNodeOfType(candidate, "JSXElement") || isNodeOfType(candidate, "JSXFragment") || isRegExpLiteral(candidate)) return true;
15465
+ if (isNodeOfType(candidate, "SequenceExpression")) {
15466
+ const finalExpression = candidate.expressions.at(-1);
15467
+ return Boolean(finalExpression && isGuaranteedFreshStateValue(finalExpression, scopes));
15252
15468
  }
15253
- const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
15254
- if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
15255
- const test = unwrapExpression$3(conditional.test);
15469
+ if (isNodeOfType(candidate, "ConditionalExpression")) return isGuaranteedFreshStateValue(candidate.consequent, scopes, visitedSymbolIds) && isGuaranteedFreshStateValue(candidate.alternate, scopes, visitedSymbolIds);
15470
+ if (isNodeOfType(candidate, "LogicalExpression")) return isGuaranteedFreshStateValue(candidate.left, scopes, visitedSymbolIds) && isGuaranteedFreshStateValue(candidate.right, scopes, visitedSymbolIds);
15471
+ if (isNodeOfType(candidate, "CallExpression")) {
15472
+ const callee = unwrapExpression$3(candidate.callee);
15473
+ return isNodeOfType(callee, "Identifier") && scopes.isGlobalReference(callee) && (callee.name === "Array" || callee.name === "Object") && (callee.name === "Array" || candidate.arguments.length === 0);
15474
+ }
15475
+ if (isNodeOfType(candidate, "NewExpression")) {
15476
+ const callee = unwrapExpression$3(candidate.callee);
15477
+ return isNodeOfType(callee, "Identifier") && scopes.isGlobalReference(callee) && FRESH_STATE_CONSTRUCTOR_NAMES.has(callee.name) && (callee.name !== "Object" || candidate.arguments.length === 0);
15478
+ }
15479
+ if (!isNodeOfType(candidate, "Identifier")) return false;
15480
+ const symbol = scopes.symbolFor(candidate);
15481
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer || isOutsideAllFunctions(symbol) || visitedSymbolIds.has(symbol.id)) return false;
15482
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
15483
+ nextVisitedSymbolIds.add(symbol.id);
15484
+ return isGuaranteedFreshStateValue(symbol.initializer, scopes, nextVisitedSymbolIds);
15485
+ };
15486
+ const isNonZeroLiteral = (node) => {
15487
+ const candidate = unwrapExpression$3(node);
15488
+ return isNodeOfType(candidate, "Literal") && (typeof candidate.value === "number" && candidate.value !== 0 || typeof candidate.value === "bigint" && candidate.value !== 0n || typeof candidate.value === "string" && candidate.value.length > 0);
15489
+ };
15490
+ const isProvablyChangingExpression = (node, previousValueSymbol, scopes) => {
15491
+ const candidate = unwrapExpression$3(node);
15492
+ if (isGuaranteedFreshStateValue(candidate, scopes)) return true;
15493
+ if (isNodeOfType(candidate, "ConditionalExpression")) {
15494
+ const isConsequentChanging = isProvablyChangingExpression(candidate.consequent, previousValueSymbol, scopes);
15495
+ const isAlternateChanging = isProvablyChangingExpression(candidate.alternate, previousValueSymbol, scopes);
15496
+ return expressionReadsSymbol(candidate.test, previousValueSymbol, scopes) ? isConsequentChanging && isAlternateChanging : isConsequentChanging || isAlternateChanging;
15497
+ }
15498
+ if (!expressionReadsSymbol(candidate, previousValueSymbol, scopes)) return false;
15499
+ if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator === "!";
15500
+ if (isNodeOfType(candidate, "UpdateExpression")) return true;
15501
+ if (!isNodeOfType(candidate, "BinaryExpression")) return false;
15502
+ return (candidate.operator === "+" || candidate.operator === "-") && expressionReadsSymbol(candidate.left, previousValueSymbol, scopes) && isNonZeroLiteral(candidate.right);
15503
+ };
15504
+ const isFreshEqualityGuardUpdater = (node, previousValueSymbol, scopes) => {
15505
+ const candidate = unwrapExpression$3(node);
15506
+ if (!isNodeOfType(candidate, "ConditionalExpression")) return false;
15507
+ const test = unwrapExpression$3(candidate.test);
15256
15508
  if (!isNodeOfType(test, "BinaryExpression") || ![
15257
15509
  "===",
15258
15510
  "!==",
@@ -15260,38 +15512,50 @@ const isConvergingFunctionalUpdater = (node, scopes) => {
15260
15512
  "!="
15261
15513
  ].includes(test.operator)) return false;
15262
15514
  const isPreviousValue = (expression) => {
15263
- const candidate = unwrapExpression$3(expression);
15264
- return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
15515
+ const expressionCandidate = unwrapExpression$3(expression);
15516
+ return isNodeOfType(expressionCandidate, "Identifier") && scopes.symbolFor(expressionCandidate)?.id === previousValueSymbol?.id;
15265
15517
  };
15266
15518
  let comparedValue = null;
15267
15519
  if (isPreviousValue(test.left)) comparedValue = test.right;
15268
- else if (isPreviousValue(test.right)) comparedValue = test.left;
15269
- if (!comparedValue) return false;
15270
- if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
15271
- const isSameComparedValue = (expression) => {
15272
- const candidate = unwrapExpression$3(expression);
15273
- const compared = unwrapExpression$3(comparedValue);
15274
- if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
15275
- if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
15276
- return false;
15520
+ if (isPreviousValue(test.right)) comparedValue = test.left;
15521
+ if (!comparedValue || !isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
15522
+ const isComparedValue = (expression) => {
15523
+ const expressionCandidate = unwrapExpression$3(expression);
15524
+ const comparedCandidate = unwrapExpression$3(comparedValue);
15525
+ if (isNodeOfType(expressionCandidate, "Identifier")) return isNodeOfType(comparedCandidate, "Identifier") && scopes.symbolFor(expressionCandidate)?.id === scopes.symbolFor(comparedCandidate)?.id;
15526
+ return isNodeOfType(expressionCandidate, "Literal") && isNodeOfType(comparedCandidate, "Literal") && expressionCandidate.value === comparedCandidate.value;
15277
15527
  };
15278
- return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
15528
+ return test.operator === "===" || test.operator === "==" ? isPreviousValue(candidate.consequent) && isComparedValue(candidate.alternate) : isComparedValue(candidate.consequent) && isPreviousValue(candidate.alternate);
15279
15529
  };
15280
- const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
15281
- const parent = identifier.parent;
15282
- if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
15283
- const writtenValue = parent.arguments?.[0];
15284
- return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
15285
- };
15286
- const findStableSetterReference = (node, scopes) => {
15530
+ const isProvablyChangingFunctionalUpdater = (node, scopes) => {
15531
+ const updater = unwrapExpression$3(node);
15532
+ if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
15533
+ const previousValueParameter = updater.params?.[0];
15534
+ if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
15535
+ const previousValueSymbol = scopes.symbolFor(previousValueParameter);
15536
+ if (!isNodeOfType(updater.body, "BlockStatement")) return isFreshEqualityGuardUpdater(updater.body, previousValueSymbol, scopes) || isProvablyChangingExpression(updater.body, previousValueSymbol, scopes);
15537
+ if (updater.body.body.length !== 1) return false;
15538
+ const soleStatement = updater.body.body[0];
15539
+ if (!isNodeOfType(soleStatement, "ReturnStatement") || !isAstNode(soleStatement.argument)) return false;
15540
+ return isFreshEqualityGuardUpdater(soleStatement.argument, previousValueSymbol, scopes) || isProvablyChangingExpression(soleStatement.argument, previousValueSymbol, scopes);
15541
+ };
15542
+ const isProvablyRenderChangingSetterCall = (identifier, scopes) => {
15543
+ const setterCall = identifier.parent;
15544
+ if (!setterCall || !isNodeOfType(setterCall, "CallExpression") || setterCall.callee !== identifier) return false;
15545
+ const descriptor = getStateSetterDescriptor(identifier, scopes);
15546
+ const writtenValue = setterCall.arguments?.[0];
15547
+ if (!descriptor || !writtenValue || !isAstNode(writtenValue)) return false;
15548
+ return isProvablyChangingFunctionalUpdater(writtenValue, scopes) ? true : isProvablyChangingExpression(writtenValue, descriptor.stateSymbol, scopes);
15549
+ };
15550
+ const findRenderChangingStateSetterName = (node, scopes) => {
15287
15551
  let setterName = null;
15288
15552
  const visit = (current) => {
15289
15553
  if (setterName) return;
15290
15554
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
15291
15555
  if (isNodeOfType(current, "Identifier")) {
15292
- const symbol = scopes.referenceFor(current)?.resolvedSymbol;
15293
- if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
15294
- setterName = symbol.name;
15556
+ const descriptor = getStateSetterDescriptor(current, scopes);
15557
+ if (descriptor && isProvablyRenderChangingSetterCall(current, scopes)) {
15558
+ setterName = descriptor.setterSymbol.name;
15295
15559
  return;
15296
15560
  }
15297
15561
  }
@@ -15618,7 +15882,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
15618
15882
  }
15619
15883
  if (!depsArgumentRaw) {
15620
15884
  if (callbackToAnalyze && EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) {
15621
- const setterName = findStableSetterReference(callbackToAnalyze, context.scopes);
15885
+ const setterName = findRenderChangingStateSetterName(callbackToAnalyze, context.scopes);
15622
15886
  if (setterName) {
15623
15887
  context.report({
15624
15888
  node: callbackToAnalyze,
@@ -20011,14 +20275,14 @@ const isIndexOfResultUsedAsMembershipTest = (node) => {
20011
20275
  if (isNegativeOneLiteral(otherOperand)) return true;
20012
20276
  return isZeroLiteral(otherOperand) && (parent.operator === ">=" || parent.operator === "<");
20013
20277
  };
20014
- const getTypeAnnotation = (node) => {
20278
+ const getTypeAnnotation$1 = (node) => {
20015
20279
  if (!node || !("typeAnnotation" in node)) return null;
20016
20280
  const annotation = node.typeAnnotation;
20017
20281
  if (!annotation || !isNodeOfType(annotation, "TSTypeAnnotation")) return null;
20018
20282
  return annotation.typeAnnotation;
20019
20283
  };
20020
20284
  const getDeclaredPropertyType = (members, propertyName) => {
20021
- for (const member of members) if (isNodeOfType(member, "TSPropertySignature") && isNodeOfType(member.key, "Identifier") && member.key.name === propertyName) return getTypeAnnotation(member);
20285
+ for (const member of members) if (isNodeOfType(member, "TSPropertySignature") && isNodeOfType(member.key, "Identifier") && member.key.name === propertyName) return getTypeAnnotation$1(member);
20022
20286
  return null;
20023
20287
  };
20024
20288
  const getArrayElementType = (typeNode) => {
@@ -20037,7 +20301,7 @@ const getDestructuredDeclaredType = (identifier) => {
20037
20301
  const property = binding.bindingIdentifier.parent;
20038
20302
  const objectPattern = property?.parent;
20039
20303
  if (!isNodeOfType(property, "Property") || !isNodeOfType(property.key, "Identifier") || !isNodeOfType(objectPattern, "ObjectPattern")) return null;
20040
- const propsType = getTypeAnnotation(objectPattern);
20304
+ const propsType = getTypeAnnotation$1(objectPattern);
20041
20305
  if (!propsType) return null;
20042
20306
  if (isNodeOfType(propsType, "TSTypeLiteral")) return getDeclaredPropertyType(propsType.members ?? [], property.key.name);
20043
20307
  if (!isNodeOfType(propsType, "TSTypeReference") || !isNodeOfType(propsType.typeName, "Identifier")) return null;
@@ -20054,7 +20318,7 @@ const getIdentifierDeclaredType = (identifier, visitedBindingIdentifiers = /* @_
20054
20318
  const binding = findVariableInitializer(identifier, identifier.name);
20055
20319
  if (!binding || visitedBindingIdentifiers.has(binding.bindingIdentifier)) return null;
20056
20320
  visitedBindingIdentifiers.add(binding.bindingIdentifier);
20057
- const directType = getTypeAnnotation(binding.bindingIdentifier);
20321
+ const directType = getTypeAnnotation$1(binding.bindingIdentifier);
20058
20322
  if (directType) return directType;
20059
20323
  const initializer = binding.initializer;
20060
20324
  if (isNodeOfType(initializer, "TSAsExpression") || isNodeOfType(initializer, "TSTypeAssertion") || isNodeOfType(initializer, "TSSatisfiesExpression")) return initializer.typeAnnotation;
@@ -27219,6 +27483,211 @@ const computeExternallyDriven = (analysis, declarator) => {
27219
27483
  return hasDeferredCallSite;
27220
27484
  };
27221
27485
  //#endregion
27486
+ //#region src/plugin/rules/state-and-effects/utils/effect/is-proven-native-read-method.ts
27487
+ const ARRAY_READ_METHOD_NAMES$1 = new Set([
27488
+ "at",
27489
+ "concat",
27490
+ "entries",
27491
+ "every",
27492
+ "filter",
27493
+ "find",
27494
+ "findIndex",
27495
+ "findLast",
27496
+ "findLastIndex",
27497
+ "flat",
27498
+ "flatMap",
27499
+ "forEach",
27500
+ "includes",
27501
+ "indexOf",
27502
+ "join",
27503
+ "keys",
27504
+ "lastIndexOf",
27505
+ "map",
27506
+ "reduce",
27507
+ "reduceRight",
27508
+ "slice",
27509
+ "some",
27510
+ "toLocaleString",
27511
+ "toReversed",
27512
+ "toSorted",
27513
+ "toSpliced",
27514
+ "toString",
27515
+ "values",
27516
+ "with"
27517
+ ]);
27518
+ const MAP_READ_METHOD_NAMES = new Set([
27519
+ "entries",
27520
+ "forEach",
27521
+ "get",
27522
+ "has",
27523
+ "keys",
27524
+ "values"
27525
+ ]);
27526
+ const SET_READ_METHOD_NAMES = new Set([
27527
+ "difference",
27528
+ "entries",
27529
+ "forEach",
27530
+ "has",
27531
+ "intersection",
27532
+ "isDisjointFrom",
27533
+ "isSubsetOf",
27534
+ "isSupersetOf",
27535
+ "keys",
27536
+ "symmetricDifference",
27537
+ "union",
27538
+ "values"
27539
+ ]);
27540
+ const FUNCTION_READ_METHOD_NAMES = new Set(["bind"]);
27541
+ const PROMISE_READ_METHOD_NAMES = new Set([
27542
+ "catch",
27543
+ "finally",
27544
+ "then"
27545
+ ]);
27546
+ const STRING_READ_METHOD_NAMES$1 = new Set([
27547
+ "at",
27548
+ "charAt",
27549
+ "charCodeAt",
27550
+ "codePointAt",
27551
+ "concat",
27552
+ "endsWith",
27553
+ "includes",
27554
+ "indexOf",
27555
+ "isWellFormed",
27556
+ "lastIndexOf",
27557
+ "localeCompare",
27558
+ "match",
27559
+ "matchAll",
27560
+ "normalize",
27561
+ "padEnd",
27562
+ "padStart",
27563
+ "repeat",
27564
+ "replace",
27565
+ "replaceAll",
27566
+ "search",
27567
+ "slice",
27568
+ "split",
27569
+ "startsWith",
27570
+ "substring",
27571
+ "toLocaleLowerCase",
27572
+ "toLocaleUpperCase",
27573
+ "toLowerCase",
27574
+ "toString",
27575
+ "toUpperCase",
27576
+ "toWellFormed",
27577
+ "trim",
27578
+ "trimEnd",
27579
+ "trimStart",
27580
+ "valueOf"
27581
+ ]);
27582
+ const NATIVE_TYPE_NAMES = new Set([
27583
+ "Array",
27584
+ "Map",
27585
+ "Promise",
27586
+ "PromiseLike",
27587
+ "ReadonlyArray",
27588
+ "ReadonlyMap",
27589
+ "ReadonlySet",
27590
+ "Set"
27591
+ ]);
27592
+ const nativeTypeDeclarationsByProgram = /* @__PURE__ */ new WeakMap();
27593
+ const shadowedNativeTypeNamesByBinding = /* @__PURE__ */ new WeakMap();
27594
+ const isWithinNode = (node, ancestor) => {
27595
+ let current = node;
27596
+ while (current) {
27597
+ if (current === ancestor) return true;
27598
+ current = current.parent;
27599
+ }
27600
+ return false;
27601
+ };
27602
+ const findTypeDeclarationScope = (declaration) => {
27603
+ let current = declaration.parent;
27604
+ while (current) {
27605
+ if (isNodeOfType(current, "Program") || isNodeOfType(current, "BlockStatement") || isNodeOfType(current, "TSModuleBlock") || isNodeOfType(current, "StaticBlock")) return current;
27606
+ current = current.parent;
27607
+ }
27608
+ return null;
27609
+ };
27610
+ const getDeclaredTypeName = (declaration) => {
27611
+ if (!isNodeOfType(declaration, "ClassDeclaration") && !isNodeOfType(declaration, "TSEnumDeclaration") && !isNodeOfType(declaration, "TSImportEqualsDeclaration") && !isNodeOfType(declaration, "TSInterfaceDeclaration") && !isNodeOfType(declaration, "TSModuleDeclaration") && !isNodeOfType(declaration, "TSTypeAliasDeclaration")) return null;
27612
+ return declaration.id && isNodeOfType(declaration.id, "Identifier") ? declaration.id.name : null;
27613
+ };
27614
+ const getNativeTypeDeclarations = (program) => {
27615
+ const cachedDeclarations = nativeTypeDeclarationsByProgram.get(program);
27616
+ if (cachedDeclarations) return cachedDeclarations;
27617
+ const declarations = [];
27618
+ walkAst(program, (candidate) => {
27619
+ if (isNodeOfType(candidate, "ImportDeclaration")) {
27620
+ for (const specifier of candidate.specifiers) if (NATIVE_TYPE_NAMES.has(specifier.local.name)) declarations.push({
27621
+ name: specifier.local.name,
27622
+ scope: program
27623
+ });
27624
+ return;
27625
+ }
27626
+ const declaredTypeName = getDeclaredTypeName(candidate);
27627
+ if (!declaredTypeName || !NATIVE_TYPE_NAMES.has(declaredTypeName)) return;
27628
+ const declarationScope = findTypeDeclarationScope(candidate);
27629
+ if (declarationScope) declarations.push({
27630
+ name: declaredTypeName,
27631
+ scope: declarationScope
27632
+ });
27633
+ });
27634
+ nativeTypeDeclarationsByProgram.set(program, declarations);
27635
+ return declarations;
27636
+ };
27637
+ const getShadowedNativeTypeNames = (binding) => {
27638
+ const cachedNames = shadowedNativeTypeNamesByBinding.get(binding);
27639
+ if (cachedNames) return cachedNames;
27640
+ const shadowedNames = /* @__PURE__ */ new Set();
27641
+ for (const typeName of NATIVE_TYPE_NAMES) if (hasEnclosingTypeParameterNamed(binding, typeName)) shadowedNames.add(typeName);
27642
+ const program = findProgramRoot(binding);
27643
+ if (program) {
27644
+ for (const declaration of getNativeTypeDeclarations(program)) if (isWithinNode(binding, declaration.scope)) shadowedNames.add(declaration.name);
27645
+ }
27646
+ shadowedNativeTypeNamesByBinding.set(binding, shadowedNames);
27647
+ return shadowedNames;
27648
+ };
27649
+ const getParameterBinding = (definitionName) => {
27650
+ if (isNodeOfType(definitionName, "Identifier")) return definitionName;
27651
+ if (!isNodeOfType(definitionName, "AssignmentPattern")) return null;
27652
+ return isNodeOfType(definitionName.left, "Identifier") ? definitionName.left : null;
27653
+ };
27654
+ const getTypeAnnotation = (binding) => {
27655
+ if (!isNodeOfType(binding, "Identifier")) return null;
27656
+ const annotation = binding.typeAnnotation;
27657
+ return annotation && isNodeOfType(annotation, "TSTypeAnnotation") ? annotation.typeAnnotation : null;
27658
+ };
27659
+ const isNullishType = (typeNode) => isNodeOfType(typeNode, "TSNullKeyword") || isNodeOfType(typeNode, "TSUndefinedKeyword");
27660
+ const isNativeReadMethod = (typeNode, methodName, shadowedTypeNames) => {
27661
+ if (isNodeOfType(typeNode, "TSArrayType") || isNodeOfType(typeNode, "TSTupleType")) return ARRAY_READ_METHOD_NAMES$1.has(methodName);
27662
+ if (isNodeOfType(typeNode, "TSStringKeyword")) return STRING_READ_METHOD_NAMES$1.has(methodName);
27663
+ if (isNodeOfType(typeNode, "TSFunctionType")) return FUNCTION_READ_METHOD_NAMES.has(methodName);
27664
+ if (isNodeOfType(typeNode, "TSLiteralType") && isNodeOfType(typeNode.literal, "Literal") && typeof typeNode.literal.value === "string") return STRING_READ_METHOD_NAMES$1.has(methodName);
27665
+ if (isNodeOfType(typeNode, "TSTypeOperator") && typeNode.operator === "readonly" && typeNode.typeAnnotation) return isNativeReadMethod(typeNode.typeAnnotation, methodName, shadowedTypeNames);
27666
+ if (isNodeOfType(typeNode, "TSUnionType")) {
27667
+ let hasNonNullishMember = false;
27668
+ for (const memberType of typeNode.types) {
27669
+ if (isNullishType(memberType)) continue;
27670
+ hasNonNullishMember = true;
27671
+ if (!isNativeReadMethod(memberType, methodName, shadowedTypeNames)) return false;
27672
+ }
27673
+ return hasNonNullishMember;
27674
+ }
27675
+ if (!isNodeOfType(typeNode, "TSTypeReference") || !isNodeOfType(typeNode.typeName, "Identifier")) return false;
27676
+ if (shadowedTypeNames.has(typeNode.typeName.name)) return false;
27677
+ if (typeNode.typeName.name === "Array" || typeNode.typeName.name === "ReadonlyArray") return ARRAY_READ_METHOD_NAMES$1.has(methodName);
27678
+ if (typeNode.typeName.name === "Map" || typeNode.typeName.name === "ReadonlyMap") return MAP_READ_METHOD_NAMES.has(methodName);
27679
+ if (typeNode.typeName.name === "Set" || typeNode.typeName.name === "ReadonlySet") return SET_READ_METHOD_NAMES.has(methodName);
27680
+ if (typeNode.typeName.name === "Promise" || typeNode.typeName.name === "PromiseLike") return PROMISE_READ_METHOD_NAMES.has(methodName);
27681
+ return false;
27682
+ };
27683
+ const isProvenNativeReadMethod = (ref, methodName) => Boolean(ref.resolved?.defs.some((definition) => {
27684
+ if (definition.type !== "Parameter") return false;
27685
+ const parameterBinding = getParameterBinding(definition.name);
27686
+ if (!parameterBinding) return false;
27687
+ const typeAnnotation = getTypeAnnotation(parameterBinding);
27688
+ return Boolean(typeAnnotation && isNativeReadMethod(typeAnnotation, methodName, getShadowedNativeTypeNames(parameterBinding)));
27689
+ }));
27690
+ //#endregion
27222
27691
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
27223
27692
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
27224
27693
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
@@ -27445,7 +27914,132 @@ const isRefCurrent = (ref) => {
27445
27914
  const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isStateSetter(analysis, innerRef));
27446
27915
  const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
27447
27916
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
27448
- const isPropCallbackInvocationRef = (analysis, ref) => {
27917
+ const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
27918
+ ["every", 0],
27919
+ ["filter", 0],
27920
+ ["find", 0],
27921
+ ["findIndex", 0],
27922
+ ["findLast", 0],
27923
+ ["findLastIndex", 0],
27924
+ ["flatMap", 0],
27925
+ ["forEach", 0],
27926
+ ["map", 0],
27927
+ ["reduce", 0],
27928
+ ["reduceRight", 0],
27929
+ ["replace", 1],
27930
+ ["replaceAll", 1],
27931
+ ["some", 0]
27932
+ ]);
27933
+ const addSimpleBindingNames = (pattern, names) => {
27934
+ if (isNodeOfType(pattern, "Identifier")) {
27935
+ if (names.has(pattern.name)) return false;
27936
+ names.add(pattern.name);
27937
+ return true;
27938
+ }
27939
+ if (isNodeOfType(pattern, "AssignmentPattern")) return addSimpleBindingNames(pattern.left, names);
27940
+ if (isNodeOfType(pattern, "RestElement")) return addSimpleBindingNames(pattern.argument, names);
27941
+ if (isNodeOfType(pattern, "ObjectPattern")) {
27942
+ let didAddName = false;
27943
+ for (const property of pattern.properties) didAddName = addSimpleBindingNames(isNodeOfType(property, "Property") ? property.value : property, names) || didAddName;
27944
+ return didAddName;
27945
+ }
27946
+ if (isNodeOfType(pattern, "ArrayPattern")) {
27947
+ let didAddName = false;
27948
+ for (const element of pattern.elements) if (element) didAddName = addSimpleBindingNames(element, names) || didAddName;
27949
+ return didAddName;
27950
+ }
27951
+ return false;
27952
+ };
27953
+ const getSimpleParameterNames = (callback) => {
27954
+ if (!isFunctionLike$1(callback)) return /* @__PURE__ */ new Set();
27955
+ const names = /* @__PURE__ */ new Set();
27956
+ for (const parameter of callback.params ?? []) addSimpleBindingNames(parameter, names);
27957
+ return names;
27958
+ };
27959
+ const inlineCallbackInvokesParameter = (callback) => {
27960
+ const parameterNames = getSimpleParameterNames(callback);
27961
+ if (parameterNames.size === 0 || !isFunctionLike$1(callback)) return false;
27962
+ let didAddAlias = true;
27963
+ while (didAddAlias) {
27964
+ didAddAlias = false;
27965
+ walkAst(callback.body, (candidate) => {
27966
+ if (candidate !== callback.body && isFunctionLike$1(candidate)) return false;
27967
+ if (isNodeOfType(candidate, "AssignmentExpression") && candidate.operator === "=" && isNodeOfType(candidate.left, "Identifier")) {
27968
+ const assignedValueRoot = getRootIdentifier$1(candidate.right);
27969
+ if (isNodeOfType(assignedValueRoot, "Identifier") && parameterNames.has(assignedValueRoot.name) && !parameterNames.has(candidate.left.name)) {
27970
+ parameterNames.add(candidate.left.name);
27971
+ didAddAlias = true;
27972
+ }
27973
+ return;
27974
+ }
27975
+ if (!isNodeOfType(candidate, "VariableDeclarator") || !candidate.init) return;
27976
+ const initializerRoot = getRootIdentifier$1(candidate.init);
27977
+ if (!isNodeOfType(initializerRoot, "Identifier") || !parameterNames.has(initializerRoot.name)) return;
27978
+ if (addSimpleBindingNames(candidate.id, parameterNames)) didAddAlias = true;
27979
+ });
27980
+ }
27981
+ let didInvokeParameter = false;
27982
+ walkAst(callback.body, (candidate) => {
27983
+ if (didInvokeParameter) return false;
27984
+ if (candidate !== callback.body && isFunctionLike$1(candidate)) return false;
27985
+ if (!isNodeOfType(candidate, "CallExpression")) return;
27986
+ const callee = stripParenExpression(candidate.callee);
27987
+ if (isNodeOfType(callee, "Identifier") && parameterNames.has(callee.name)) {
27988
+ didInvokeParameter = true;
27989
+ return false;
27990
+ }
27991
+ if (isNodeOfType(callee, "MemberExpression")) {
27992
+ const receiverRoot = getRootIdentifier$1(callee.object);
27993
+ if (receiverRoot && parameterNames.has(receiverRoot.name)) {
27994
+ didInvokeParameter = true;
27995
+ return false;
27996
+ }
27997
+ }
27998
+ });
27999
+ return didInvokeParameter;
28000
+ };
28001
+ const callbackInvokesPropCallback = (analysis, callback) => {
28002
+ if (!isFunctionLike$1(callback)) return false;
28003
+ let didInvokePropCallback = false;
28004
+ walkAst(callback.body, (candidate) => {
28005
+ if (didInvokePropCallback) return false;
28006
+ if (candidate !== callback.body && isFunctionLike$1(candidate)) return false;
28007
+ if (!isNodeOfType(candidate, "CallExpression")) return;
28008
+ if (getDownstreamRefs(analysis, stripParenExpression(candidate.callee)).some((reference) => isPropCallbackInvocationRef(analysis, reference))) {
28009
+ didInvokePropCallback = true;
28010
+ return false;
28011
+ }
28012
+ });
28013
+ return didInvokePropCallback;
28014
+ };
28015
+ const isNativeMethodSuppressionSafe = (analysis, callExpression, methodName, scopes) => {
28016
+ if (!isNodeOfType(callExpression, "CallExpression")) return false;
28017
+ const callee = stripParenExpression(callExpression.callee);
28018
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
28019
+ const receiver = stripParenExpression(callee.object);
28020
+ if (!isNodeOfType(receiver, "Identifier")) return false;
28021
+ const receiverSymbol = scopes.symbolFor(receiver);
28022
+ if (!receiverSymbol || hasSymbolWriteBefore(receiverSymbol, callExpression, scopes) || hasPossibleStaticPropertyWriteBefore(receiver, methodName, callExpression, scopes)) return false;
28023
+ let callResult = callExpression;
28024
+ let callResultConsumer = callResult.parent;
28025
+ while (callResultConsumer && (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(callResultConsumer.type) && "expression" in callResultConsumer && callResultConsumer.expression === callResult || isNodeOfType(callResultConsumer, "MemberExpression") && callResultConsumer.object === callResult)) {
28026
+ callResult = callResultConsumer;
28027
+ callResultConsumer = callResult.parent;
28028
+ }
28029
+ if (isNodeOfType(callResultConsumer, "CallExpression") && callResultConsumer.callee === callResult) return false;
28030
+ const callbackArgumentIndex = SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD.get(methodName);
28031
+ if (callbackArgumentIndex === void 0) return true;
28032
+ const callbackArgument = callExpression.arguments?.[callbackArgumentIndex];
28033
+ if (!callbackArgument) return true;
28034
+ const callbackValue = stripParenExpression(callbackArgument);
28035
+ if (isFunctionLike$1(callbackValue)) return !inlineCallbackInvokesParameter(callbackValue) && !callbackInvokesPropCallback(analysis, callbackValue);
28036
+ if (!isNodeOfType(callbackValue, "Identifier")) return false;
28037
+ const callbackRef = getRef(analysis, callbackValue);
28038
+ if (!callbackRef || isPropAlias(analysis, callbackRef)) return false;
28039
+ const callbackFunction = resolveToFunction(callbackRef);
28040
+ return Boolean(callbackFunction && isFunctionLike$1(callbackFunction) && !inlineCallbackInvokesParameter(callbackFunction) && !callbackInvokesPropCallback(analysis, callbackFunction));
28041
+ };
28042
+ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
27449
28043
  if (!isPropAlias(analysis, ref)) return false;
27450
28044
  let effectiveNode = ref.identifier;
27451
28045
  let parent = effectiveNode.parent;
@@ -27458,7 +28052,9 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
27458
28052
  if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
27459
28053
  const memberParent = parent.parent;
27460
28054
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
27461
- if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
28055
+ const propertyName = getStaticPropertyName(parent);
28056
+ if (propertyName && HANDLER_NAMED_METHOD_PATTERN.test(propertyName)) return true;
28057
+ if (options.nativeMethodScopes && isCustomHookParameter(ref) && propertyName && isProvenNativeReadMethod(ref, propertyName) && isNativeMethodSuppressionSafe(analysis, memberParent, propertyName, options.nativeMethodScopes)) return false;
27462
28058
  return isWholePropsObjectReference(analysis, ref);
27463
28059
  }
27464
28060
  }
@@ -30652,6 +31248,514 @@ const noCascadingSetState = defineRetiredRule({
30652
31248
  recommendation: "Retired: React batches synchronous state updates from one effect into the same follow-up commit, so setter count does not prove repeated redraws."
30653
31249
  });
30654
31250
  //#endregion
31251
+ //#region src/plugin/rules/state-and-effects/utils/create-state-trigger-reachability.ts
31252
+ const getRefCurrentSymbol = (node, context) => {
31253
+ const expression = stripParenExpression(node);
31254
+ if (!isNodeOfType(expression, "MemberExpression") || getStaticMemberPropertyName(expression) !== "current" || !isNodeOfType(expression.object, "Identifier")) return null;
31255
+ return context.scopes.symbolFor(expression.object);
31256
+ };
31257
+ const expressionReadsState = (analysis, expression) => getDownstreamRefs(analysis, expression).some((reference) => getUpstreamRefs(analysis, reference).some((upstreamReference) => isState(analysis, upstreamReference)));
31258
+ const expressionReadsProp = (analysis, expression) => getDownstreamRefs(analysis, expression).some((reference) => getUpstreamRefs(analysis, reference).some((upstreamReference) => isProp(analysis, upstreamReference)));
31259
+ const hasNonInitializerWrite = (analysis, identifier) => {
31260
+ const reference = getRef(analysis, identifier);
31261
+ return Boolean(reference?.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
31262
+ };
31263
+ const isStableParameterDefault = (expression) => {
31264
+ const candidate = stripParenExpression(expression);
31265
+ if (isNodeOfType(candidate, "Literal")) return candidate.value === null || [
31266
+ "bigint",
31267
+ "boolean",
31268
+ "number",
31269
+ "string"
31270
+ ].includes(typeof candidate.value);
31271
+ return Boolean(isNodeOfType(candidate, "TemplateLiteral") && (candidate.expressions ?? []).length === 0);
31272
+ };
31273
+ const isDirectComponentPropBinding = (symbol) => {
31274
+ const parameter = symbol.declarationNode;
31275
+ if (parameter === symbol.bindingIdentifier) return true;
31276
+ if (isNodeOfType(parameter, "AssignmentPattern")) return parameter.left === symbol.bindingIdentifier && isStableParameterDefault(parameter.right);
31277
+ if (!isNodeOfType(parameter, "ObjectPattern")) return false;
31278
+ return parameter.properties.some((property) => {
31279
+ if (!isNodeOfType(property, "Property")) return false;
31280
+ const propertyValue = property.value;
31281
+ if (propertyValue === symbol.bindingIdentifier) return true;
31282
+ return Boolean(isNodeOfType(propertyValue, "AssignmentPattern") && propertyValue.left === symbol.bindingIdentifier && isStableParameterDefault(propertyValue.right));
31283
+ });
31284
+ };
31285
+ const REFLEXIVE_TYPE_NODE_TYPES = new Set([
31286
+ "TSBigIntKeyword",
31287
+ "TSBooleanKeyword",
31288
+ "TSNeverKeyword",
31289
+ "TSNullKeyword",
31290
+ "TSObjectKeyword",
31291
+ "TSStringKeyword",
31292
+ "TSSymbolKeyword",
31293
+ "TSUndefinedKeyword"
31294
+ ]);
31295
+ const PRIMITIVE_TYPE_NODE_TYPES = new Set([
31296
+ "TSBigIntKeyword",
31297
+ "TSBooleanKeyword",
31298
+ "TSNeverKeyword",
31299
+ "TSNullKeyword",
31300
+ "TSNumberKeyword",
31301
+ "TSStringKeyword",
31302
+ "TSSymbolKeyword",
31303
+ "TSUndefinedKeyword"
31304
+ ]);
31305
+ const isDefinitelyReflexiveType = (typeNode) => {
31306
+ if (REFLEXIVE_TYPE_NODE_TYPES.has(typeNode.type)) return true;
31307
+ if (isNodeOfType(typeNode, "TSLiteralType")) return Boolean(isNodeOfType(typeNode.literal, "Literal") && (typeof typeNode.literal.value !== "number" || !Number.isNaN(typeNode.literal.value)));
31308
+ if (isNodeOfType(typeNode, "TSUnionType")) return (typeNode.types ?? []).every((member) => isDefinitelyReflexiveType(member));
31309
+ return false;
31310
+ };
31311
+ const isDefinitelyPrimitiveType = (typeNode) => {
31312
+ if (PRIMITIVE_TYPE_NODE_TYPES.has(typeNode.type)) return true;
31313
+ if (isNodeOfType(typeNode, "TSLiteralType")) return true;
31314
+ if (isNodeOfType(typeNode, "TSUnionType")) return (typeNode.types ?? []).every((member) => isDefinitelyPrimitiveType(member));
31315
+ return false;
31316
+ };
31317
+ const isDefinitelyPrimitiveExpression = (expression, context) => {
31318
+ const candidate = stripParenExpression(expression);
31319
+ if (isNodeOfType(candidate, "Literal")) return true;
31320
+ if (!isNodeOfType(candidate, "Identifier")) return false;
31321
+ const symbol = context.scopes.symbolFor(candidate);
31322
+ const typeAnnotation = symbol ? getSymbolTypeAnnotation(symbol) : null;
31323
+ return Boolean(typeAnnotation && isDefinitelyPrimitiveType(typeAnnotation));
31324
+ };
31325
+ const isDefinitelyReflexiveExpression = (expression, analysis, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
31326
+ const candidate = stripParenExpression(expression);
31327
+ if (isNodeOfType(candidate, "Literal")) return typeof candidate.value !== "number" || !Number.isNaN(candidate.value);
31328
+ if (isNodeOfType(candidate, "TemplateLiteral") || isNodeOfType(candidate, "ArrayExpression") || isNodeOfType(candidate, "ObjectExpression") || isFunctionLike$1(candidate)) return true;
31329
+ if (isNodeOfType(candidate, "UnaryExpression")) return [
31330
+ "!",
31331
+ "typeof",
31332
+ "void",
31333
+ "~"
31334
+ ].includes(candidate.operator);
31335
+ if (isNodeOfType(candidate, "BinaryExpression")) return [
31336
+ "&",
31337
+ "|",
31338
+ "^",
31339
+ "<<",
31340
+ ">>",
31341
+ ">>>"
31342
+ ].includes(candidate.operator);
31343
+ if (isNodeOfType(candidate, "CallExpression")) {
31344
+ const callee = stripParenExpression(candidate.callee);
31345
+ return Boolean(isNodeOfType(callee, "Identifier") && ["Boolean", "String"].includes(callee.name) && context.scopes.isGlobalReference(callee));
31346
+ }
31347
+ if (!isNodeOfType(candidate, "Identifier")) return false;
31348
+ if (candidate.name === "undefined" && context.scopes.isGlobalReference(candidate)) return true;
31349
+ const symbol = context.scopes.symbolFor(candidate);
31350
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
31351
+ const typeAnnotation = getSymbolTypeAnnotation(symbol);
31352
+ if (typeAnnotation && isDefinitelyReflexiveType(typeAnnotation)) return true;
31353
+ if (!symbol.initializer || hasNonInitializerWrite(analysis, candidate)) return false;
31354
+ const nextVisitedSymbolIds = new Set(visitedSymbolIds);
31355
+ nextVisitedSymbolIds.add(symbol.id);
31356
+ return isDefinitelyReflexiveExpression(symbol.initializer, analysis, context, nextVisitedSymbolIds);
31357
+ };
31358
+ const isSupportedPropProjection = (analysis, expression, context, visitedBindings = /* @__PURE__ */ new Set()) => {
31359
+ const candidate = stripParenExpression(expression);
31360
+ if (isNodeOfType(candidate, "Identifier")) {
31361
+ const reference = getRef(analysis, candidate);
31362
+ if (!reference) return false;
31363
+ if (isProp(analysis, reference)) {
31364
+ if (isCustomHookParameter(reference)) return false;
31365
+ const symbol = context.scopes.symbolFor(candidate);
31366
+ return Boolean(symbol && isDirectComponentPropBinding(symbol));
31367
+ }
31368
+ if (!reference.resolved || visitedBindings.has(reference.resolved)) return false;
31369
+ if (hasNonInitializerWrite(analysis, candidate)) return false;
31370
+ const declarator = reference.resolved.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
31371
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return false;
31372
+ const nextVisitedBindings = new Set(visitedBindings);
31373
+ nextVisitedBindings.add(reference.resolved);
31374
+ return isSupportedPropProjection(analysis, declarator.init, context, nextVisitedBindings);
31375
+ }
31376
+ if (isNodeOfType(candidate, "MemberExpression")) return false;
31377
+ if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "~") return Boolean(isDefinitelyPrimitiveExpression(candidate.argument, context) && isSupportedPropProjection(analysis, candidate.argument, context, visitedBindings));
31378
+ if (isNodeOfType(candidate, "BinaryExpression") && [
31379
+ "&",
31380
+ "|",
31381
+ "^",
31382
+ "<<",
31383
+ ">>",
31384
+ ">>>"
31385
+ ].includes(candidate.operator)) return [candidate.left, candidate.right].every((operand) => {
31386
+ const projectionOperand = stripParenExpression(operand);
31387
+ return isNodeOfType(projectionOperand, "Literal") || isDefinitelyPrimitiveExpression(projectionOperand, context) && isSupportedPropProjection(analysis, projectionOperand, context, visitedBindings);
31388
+ });
31389
+ if (isNodeOfType(candidate, "CallExpression")) {
31390
+ const callee = stripParenExpression(candidate.callee);
31391
+ if (isNodeOfType(callee, "Identifier") && ["Boolean", "String"].includes(callee.name) && context.scopes.isGlobalReference(callee) && (candidate.arguments ?? []).length === 1) {
31392
+ const argument = candidate.arguments?.[0];
31393
+ return Boolean(argument && isDefinitelyPrimitiveExpression(argument, context) && isSupportedPropProjection(analysis, argument, context, visitedBindings));
31394
+ }
31395
+ return false;
31396
+ }
31397
+ return false;
31398
+ };
31399
+ const getStableCurrentValueKey = (analysis, expression, context) => {
31400
+ if (expressionReadsState(analysis, expression)) return null;
31401
+ if (!expressionReadsProp(analysis, expression)) return null;
31402
+ if (!isSupportedPropProjection(analysis, expression, context)) return null;
31403
+ return resolveExpressionKey$1(expression, context);
31404
+ };
31405
+ const getSnapshotShape = (analysis, expression, context) => {
31406
+ const candidate = stripParenExpression(expression);
31407
+ if (isNodeOfType(candidate, "ArrayExpression")) {
31408
+ const elementKeys = [];
31409
+ for (const element of candidate.elements ?? []) {
31410
+ if (!element || isNodeOfType(element, "SpreadElement")) return null;
31411
+ const elementKey = getStableCurrentValueKey(analysis, element, context);
31412
+ if (!elementKey) return null;
31413
+ elementKeys.push(elementKey);
31414
+ }
31415
+ return {
31416
+ scalarKey: null,
31417
+ elementKeys
31418
+ };
31419
+ }
31420
+ const scalarKey = getStableCurrentValueKey(analysis, candidate, context);
31421
+ return scalarKey ? {
31422
+ scalarKey,
31423
+ elementKeys: null
31424
+ } : null;
31425
+ };
31426
+ const snapshotShapesMatch = (left, right) => {
31427
+ if (left.scalarKey !== right.scalarKey) return false;
31428
+ if (left.elementKeys === null || right.elementKeys === null) return left.elementKeys === right.elementKeys;
31429
+ return left.elementKeys.length === right.elementKeys.length && left.elementKeys.every((elementKey, index) => elementKey === right.elementKeys?.[index]);
31430
+ };
31431
+ const collectSnapshotEnvironment = (analysis, effectFunction, context) => {
31432
+ const assignmentsByRefSymbolId = /* @__PURE__ */ new Map();
31433
+ const refSymbolsById = /* @__PURE__ */ new Map();
31434
+ walkAst(effectFunction, (child) => {
31435
+ if (child !== effectFunction && isFunctionLike$1(child)) return false;
31436
+ if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=") return;
31437
+ const refSymbol = getRefCurrentSymbol(child.left, context);
31438
+ if (!refSymbol) return;
31439
+ const assignments = assignmentsByRefSymbolId.get(refSymbol.id) ?? [];
31440
+ assignments.push(child);
31441
+ assignmentsByRefSymbolId.set(refSymbol.id, assignments);
31442
+ refSymbolsById.set(refSymbol.id, refSymbol);
31443
+ });
31444
+ const refShapes = /* @__PURE__ */ new Map();
31445
+ for (const [refSymbolId, assignments] of assignmentsByRefSymbolId) {
31446
+ if (assignments.length !== 1) continue;
31447
+ const assignment = assignments[0];
31448
+ if (!assignment || !isNodeOfType(assignment, "AssignmentExpression")) continue;
31449
+ if (context.cfg.enclosingFunction(assignment) !== effectFunction) continue;
31450
+ if (!context.cfg.isUnconditionalFromEntry(assignment)) continue;
31451
+ const declarator = refSymbolsById.get(refSymbolId)?.declarationNode ?? null;
31452
+ if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useRef") || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) continue;
31453
+ const refIdentifier = isNodeOfType(declarator.id, "Identifier") ? declarator.id : null;
31454
+ const declarationSymbol = refIdentifier ? context.scopes.symbolFor(refIdentifier) : null;
31455
+ if (!declarationSymbol || declarationSymbol.id !== refSymbolId) continue;
31456
+ if (declarationSymbol.references.some((reference) => {
31457
+ const member = reference.identifier.parent;
31458
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== reference.identifier || getStaticMemberPropertyName(member) !== "current") return true;
31459
+ const parent = member.parent;
31460
+ if (!parent) return false;
31461
+ if (isNodeOfType(parent, "UpdateExpression")) return true;
31462
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === member) return parent !== assignment;
31463
+ return false;
31464
+ })) continue;
31465
+ const initializer = declarator.init.arguments?.[0];
31466
+ if (!initializer) continue;
31467
+ const initialShape = getSnapshotShape(analysis, initializer, context);
31468
+ const assignedShape = getSnapshotShape(analysis, assignment.right, context);
31469
+ if (!initialShape || !assignedShape || !snapshotShapesMatch(initialShape, assignedShape)) continue;
31470
+ refShapes.set(refSymbolId, assignedShape);
31471
+ }
31472
+ const previousValueKeys = /* @__PURE__ */ new Map();
31473
+ walkAst(effectFunction, (child) => {
31474
+ if (child !== effectFunction && isFunctionLike$1(child)) return false;
31475
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
31476
+ const refSymbol = getRefCurrentSymbol(child.init, context);
31477
+ if (!refSymbol) return;
31478
+ const snapshotShape = refShapes.get(refSymbol.id);
31479
+ if (!snapshotShape) return;
31480
+ if (isNodeOfType(child.id, "Identifier") && snapshotShape.scalarKey) {
31481
+ const previousSymbol = context.scopes.symbolFor(child.id);
31482
+ if (previousSymbol && previousSymbol.kind === "const") previousValueKeys.set(previousSymbol.id, snapshotShape.scalarKey);
31483
+ return;
31484
+ }
31485
+ if (!isNodeOfType(child.id, "ArrayPattern") || !snapshotShape.elementKeys) return;
31486
+ for (let elementIndex = 0; elementIndex < child.id.elements.length; elementIndex += 1) {
31487
+ const element = child.id.elements[elementIndex];
31488
+ const elementKey = snapshotShape.elementKeys[elementIndex];
31489
+ if (!element || !elementKey || !isNodeOfType(element, "Identifier")) continue;
31490
+ const previousSymbol = context.scopes.symbolFor(element);
31491
+ if (previousSymbol && previousSymbol.kind === "const") previousValueKeys.set(previousSymbol.id, elementKey);
31492
+ }
31493
+ });
31494
+ return {
31495
+ refShapes,
31496
+ previousValueKeys
31497
+ };
31498
+ };
31499
+ const getSnapshotValueKey = (expression, environment) => {
31500
+ const candidate = stripParenExpression(expression);
31501
+ const refSymbol = getRefCurrentSymbol(candidate, environment.context);
31502
+ if (refSymbol) return environment.snapshotEnvironment.refShapes.get(refSymbol.id)?.scalarKey ?? null;
31503
+ if (!isNodeOfType(candidate, "Identifier")) return null;
31504
+ const symbol = environment.context.scopes.symbolFor(candidate);
31505
+ if (!symbol) return null;
31506
+ const substitutedExpression = environment.substitutions.get(symbol.id);
31507
+ if (substitutedExpression) return getSnapshotValueKey(substitutedExpression, environment);
31508
+ const directKey = environment.snapshotEnvironment.previousValueKeys.get(symbol.id);
31509
+ if (directKey) return directKey;
31510
+ if (symbol.kind !== "const" || !symbol.initializer || environment.visitedSymbolIds.has(symbol.id)) return null;
31511
+ const nextVisitedSymbolIds = new Set(environment.visitedSymbolIds);
31512
+ nextVisitedSymbolIds.add(symbol.id);
31513
+ return getSnapshotValueKey(symbol.initializer, {
31514
+ ...environment,
31515
+ visitedSymbolIds: nextVisitedSymbolIds
31516
+ });
31517
+ };
31518
+ const evaluateComparison = (expression, environment) => {
31519
+ if (!isNodeOfType(expression, "BinaryExpression")) return null;
31520
+ if (![
31521
+ "===",
31522
+ "!==",
31523
+ "==",
31524
+ "!="
31525
+ ].includes(expression.operator)) return null;
31526
+ const resolveSubstitutedExpression = (operand) => {
31527
+ const candidate = stripParenExpression(operand);
31528
+ if (!isNodeOfType(candidate, "Identifier")) return candidate;
31529
+ const symbol = environment.context.scopes.symbolFor(candidate);
31530
+ return (symbol && environment.substitutions.get(symbol.id)) ?? candidate;
31531
+ };
31532
+ const leftExpression = resolveSubstitutedExpression(expression.left);
31533
+ const rightExpression = resolveSubstitutedExpression(expression.right);
31534
+ const leftSnapshotKey = getSnapshotValueKey(leftExpression, environment);
31535
+ const rightSnapshotKey = getSnapshotValueKey(rightExpression, environment);
31536
+ const leftCurrentKey = getStableCurrentValueKey(environment.analysis, leftExpression, environment.context);
31537
+ const rightCurrentKey = getStableCurrentValueKey(environment.analysis, rightExpression, environment.context);
31538
+ const currentExpression = leftSnapshotKey !== null && leftSnapshotKey === rightCurrentKey ? rightExpression : rightSnapshotKey !== null && rightSnapshotKey === leftCurrentKey ? leftExpression : null;
31539
+ if (!currentExpression || !isDefinitelyReflexiveExpression(currentExpression, environment.analysis, environment.context)) return null;
31540
+ return expression.operator === "===" || expression.operator === "==";
31541
+ };
31542
+ const hasDirectGlobalObjectIsWrite = (objectIdentifier, context) => {
31543
+ const program = findProgramRoot(objectIdentifier);
31544
+ if (!program) return true;
31545
+ let didFindWrite = false;
31546
+ walkAst(program, (candidate) => {
31547
+ if (didFindWrite) return false;
31548
+ let writeTarget = null;
31549
+ if (isNodeOfType(candidate, "AssignmentExpression")) writeTarget = candidate.left;
31550
+ else if (isNodeOfType(candidate, "UpdateExpression")) writeTarget = candidate.argument;
31551
+ else if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "delete") writeTarget = candidate.argument;
31552
+ if (!writeTarget) return;
31553
+ const target = stripParenExpression(writeTarget);
31554
+ if (isNodeOfType(target, "Identifier")) {
31555
+ didFindWrite = target.name === "Object" && context.scopes.isGlobalReference(target);
31556
+ return;
31557
+ }
31558
+ if (isNodeOfType(target, "MemberExpression") && isNodeOfType(target.object, "Identifier") && target.object.name === "Object" && context.scopes.isGlobalReference(target.object)) {
31559
+ const propertyName = getStaticMemberPropertyName(target);
31560
+ didFindWrite = propertyName === null || propertyName === "is";
31561
+ }
31562
+ });
31563
+ return didFindWrite;
31564
+ };
31565
+ const evaluateObjectIs = (expression, environment) => {
31566
+ if (!isNodeOfType(expression, "CallExpression")) return null;
31567
+ const callee = stripParenExpression(expression.callee);
31568
+ if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !environment.context.scopes.isGlobalReference(callee.object) || getStaticMemberPropertyName(callee) !== "is") return null;
31569
+ if (hasPossibleStaticPropertyWrite(callee.object, "is", environment.context.scopes) || hasDirectGlobalObjectIsWrite(callee.object, environment.context)) return null;
31570
+ const argumentsToCompare = expression.arguments ?? [];
31571
+ if (argumentsToCompare.length !== 2) return null;
31572
+ const resolveSubstitutedExpression = (operand) => {
31573
+ const candidate = stripParenExpression(operand);
31574
+ if (!isNodeOfType(candidate, "Identifier")) return candidate;
31575
+ const symbol = environment.context.scopes.symbolFor(candidate);
31576
+ return (symbol && environment.substitutions.get(symbol.id)) ?? candidate;
31577
+ };
31578
+ const leftExpression = resolveSubstitutedExpression(argumentsToCompare[0]);
31579
+ const rightExpression = resolveSubstitutedExpression(argumentsToCompare[1]);
31580
+ const leftSnapshotKey = getSnapshotValueKey(leftExpression, environment);
31581
+ const rightSnapshotKey = getSnapshotValueKey(rightExpression, environment);
31582
+ const leftCurrentKey = getStableCurrentValueKey(environment.analysis, leftExpression, environment.context);
31583
+ const rightCurrentKey = getStableCurrentValueKey(environment.analysis, rightExpression, environment.context);
31584
+ return leftSnapshotKey !== null && leftSnapshotKey === rightCurrentKey ? true : rightSnapshotKey !== null && rightSnapshotKey === leftCurrentKey ? true : null;
31585
+ };
31586
+ const getHelperReturnExpression = (functionNode) => {
31587
+ if (!isFunctionLike$1(functionNode)) return null;
31588
+ if (Boolean(functionNode.async) || Boolean(functionNode.generator)) return null;
31589
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) return functionNode.body;
31590
+ const statements = functionNode.body.body ?? [];
31591
+ if (statements.length !== 1 || !isNodeOfType(statements[0], "ReturnStatement")) return null;
31592
+ return statements[0].argument ? statements[0].argument : null;
31593
+ };
31594
+ const evaluateBoolean = (expression, environment) => {
31595
+ const candidate = stripParenExpression(expression);
31596
+ if (isNodeOfType(candidate, "Literal") && typeof candidate.value === "boolean") return candidate.value;
31597
+ if (isNodeOfType(candidate, "Identifier")) {
31598
+ const symbol = environment.context.scopes.symbolFor(candidate);
31599
+ if (!symbol) return null;
31600
+ const substitutedExpression = environment.substitutions.get(symbol.id);
31601
+ if (substitutedExpression) return evaluateBoolean(substitutedExpression, environment);
31602
+ if (symbol.kind !== "const" || !symbol.initializer || environment.visitedSymbolIds.has(symbol.id)) return null;
31603
+ const nextVisitedSymbolIds = new Set(environment.visitedSymbolIds);
31604
+ nextVisitedSymbolIds.add(symbol.id);
31605
+ return evaluateBoolean(symbol.initializer, {
31606
+ ...environment,
31607
+ visitedSymbolIds: nextVisitedSymbolIds
31608
+ });
31609
+ }
31610
+ if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "!") {
31611
+ const argumentValue = evaluateBoolean(candidate.argument, environment);
31612
+ return argumentValue === null ? null : !argumentValue;
31613
+ }
31614
+ if (isNodeOfType(candidate, "LogicalExpression")) {
31615
+ const leftValue = evaluateBoolean(candidate.left, environment);
31616
+ if (candidate.operator === "&&") {
31617
+ if (leftValue === false) return false;
31618
+ const rightValue = evaluateBoolean(candidate.right, environment);
31619
+ if (leftValue === true) return rightValue;
31620
+ return rightValue === false ? false : null;
31621
+ }
31622
+ if (candidate.operator === "||") {
31623
+ if (leftValue === true) return true;
31624
+ const rightValue = evaluateBoolean(candidate.right, environment);
31625
+ if (leftValue === false) return rightValue;
31626
+ return rightValue === true ? true : null;
31627
+ }
31628
+ return null;
31629
+ }
31630
+ const objectIsValue = evaluateObjectIs(candidate, environment);
31631
+ if (objectIsValue !== null) return objectIsValue;
31632
+ const comparisonValue = evaluateComparison(candidate, environment);
31633
+ if (comparisonValue !== null) return comparisonValue;
31634
+ if (!environment.allowHelperCall || !isNodeOfType(candidate, "CallExpression")) return null;
31635
+ const callee = stripParenExpression(candidate.callee);
31636
+ if (!isNodeOfType(callee, "Identifier")) return null;
31637
+ const calleeReference = getRef(environment.analysis, callee);
31638
+ if (calleeReference?.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
31639
+ const helperFunction = calleeReference ? resolveToFunction(calleeReference) : null;
31640
+ const returnExpression = helperFunction ? getHelperReturnExpression(helperFunction) : null;
31641
+ if (!helperFunction || !returnExpression || !isFunctionLike$1(helperFunction)) return null;
31642
+ const parameters = helperFunction.params ?? [];
31643
+ const argumentsForHelper = candidate.arguments ?? [];
31644
+ if (parameters.length !== argumentsForHelper.length) return null;
31645
+ const substitutions = new Map(environment.substitutions);
31646
+ for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
31647
+ const parameter = parameters[parameterIndex];
31648
+ const argument = argumentsForHelper[parameterIndex];
31649
+ if (!parameter || !argument || !isNodeOfType(parameter, "Identifier")) return null;
31650
+ const parameterSymbol = environment.context.scopes.symbolFor(parameter);
31651
+ if (!parameterSymbol) return null;
31652
+ substitutions.set(parameterSymbol.id, argument);
31653
+ }
31654
+ return evaluateBoolean(returnExpression, {
31655
+ ...environment,
31656
+ substitutions,
31657
+ visitedSymbolIds: /* @__PURE__ */ new Set(),
31658
+ allowHelperCall: false
31659
+ });
31660
+ };
31661
+ const statementCanCompleteNormally = (statement, environment) => {
31662
+ if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ContinueStatement")) return false;
31663
+ if (isNodeOfType(statement, "BlockStatement")) return (statement.body ?? []).every((childStatement) => statementCanCompleteNormally(childStatement, environment));
31664
+ if (isNodeOfType(statement, "IfStatement")) {
31665
+ const testValue = evaluateBoolean(statement.test, environment);
31666
+ if (testValue === true) return statementCanCompleteNormally(statement.consequent, environment);
31667
+ if (testValue === false) return statement.alternate ? statementCanCompleteNormally(statement.alternate, environment) : true;
31668
+ const consequentCanComplete = statementCanCompleteNormally(statement.consequent, environment);
31669
+ const alternateCanComplete = statement.alternate ? statementCanCompleteNormally(statement.alternate, environment) : true;
31670
+ return consequentCanComplete || alternateCanComplete;
31671
+ }
31672
+ if (isNodeOfType(statement, "LabeledStatement")) {
31673
+ const labelName = isNodeOfType(statement.label, "Identifier") ? statement.label.name : null;
31674
+ if (labelName && statementCanCompleteWithBreakToLabel(statement.body, labelName, environment)) return true;
31675
+ return statementCanCompleteNormallyForLabel(statement.body, environment);
31676
+ }
31677
+ return true;
31678
+ };
31679
+ const statementCanCompleteNormallyForLabel = (statement, environment) => {
31680
+ if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ContinueStatement")) return false;
31681
+ if (isNodeOfType(statement, "BlockStatement")) return (statement.body ?? []).every((childStatement) => statementCanCompleteNormallyForLabel(childStatement, environment));
31682
+ if (isNodeOfType(statement, "IfStatement")) {
31683
+ const testValue = evaluateBoolean(statement.test, environment);
31684
+ if (testValue === true) return statementCanCompleteNormallyForLabel(statement.consequent, environment);
31685
+ if (testValue === false) return statement.alternate ? statementCanCompleteNormallyForLabel(statement.alternate, environment) : true;
31686
+ const consequentCanComplete = statementCanCompleteNormallyForLabel(statement.consequent, environment);
31687
+ const alternateCanComplete = statement.alternate ? statementCanCompleteNormallyForLabel(statement.alternate, environment) : true;
31688
+ return consequentCanComplete || alternateCanComplete;
31689
+ }
31690
+ if (isNodeOfType(statement, "LabeledStatement")) return statementCanCompleteNormally(statement, environment);
31691
+ return isNodeOfType(statement, "ExpressionStatement") || isNodeOfType(statement, "VariableDeclaration") || isNodeOfType(statement, "EmptyStatement") || isNodeOfType(statement, "DebuggerStatement");
31692
+ };
31693
+ const statementCanCompleteWithBreakToLabel = (statement, labelName, environment) => {
31694
+ if (isNodeOfType(statement, "BreakStatement")) return Boolean(isNodeOfType(statement.label, "Identifier") && statement.label.name === labelName);
31695
+ if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement")) return false;
31696
+ if (isNodeOfType(statement, "BlockStatement")) {
31697
+ for (const childStatement of statement.body ?? []) {
31698
+ if (statementCanCompleteWithBreakToLabel(childStatement, labelName, environment)) return true;
31699
+ if (!statementCanCompleteNormallyForLabel(childStatement, environment)) return false;
31700
+ }
31701
+ return false;
31702
+ }
31703
+ if (isNodeOfType(statement, "IfStatement")) {
31704
+ const testValue = evaluateBoolean(statement.test, environment);
31705
+ if (testValue === true) return statementCanCompleteWithBreakToLabel(statement.consequent, labelName, environment);
31706
+ if (testValue === false) return Boolean(statement.alternate && statementCanCompleteWithBreakToLabel(statement.alternate, labelName, environment));
31707
+ return statementCanCompleteWithBreakToLabel(statement.consequent, labelName, environment) || Boolean(statement.alternate && statementCanCompleteWithBreakToLabel(statement.alternate, labelName, environment));
31708
+ }
31709
+ if (isNodeOfType(statement, "LabeledStatement")) return statementCanCompleteWithBreakToLabel(statement.body, labelName, environment);
31710
+ return false;
31711
+ };
31712
+ const isReachableUnderSnapshotEnvironment = (target, effectFunction, environment) => {
31713
+ if (environment.context.cfg.enclosingFunction(target) !== effectFunction) return true;
31714
+ let current = target;
31715
+ while (current !== effectFunction) {
31716
+ const parent = current.parent;
31717
+ if (!parent) return true;
31718
+ if (isFunctionLike$1(parent) && parent !== effectFunction) return true;
31719
+ if (isNodeOfType(parent, "IfStatement")) {
31720
+ const testValue = evaluateBoolean(parent.test, environment);
31721
+ if (isAstDescendant(current, parent.consequent) && testValue === false) return false;
31722
+ if (parent.alternate && isAstDescendant(current, parent.alternate) && testValue === true) return false;
31723
+ }
31724
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === current) {
31725
+ const leftValue = evaluateBoolean(parent.left, environment);
31726
+ if (parent.operator === "&&" && leftValue === false) return false;
31727
+ if (parent.operator === "||" && leftValue === true) return false;
31728
+ }
31729
+ if (isNodeOfType(parent, "ConditionalExpression")) {
31730
+ const testValue = evaluateBoolean(parent.test, environment);
31731
+ if (parent.consequent === current && testValue === false) return false;
31732
+ if (parent.alternate === current && testValue === true) return false;
31733
+ }
31734
+ if (isNodeOfType(parent, "BlockStatement")) {
31735
+ const containingStatementIndex = (parent.body ?? []).findIndex((statement) => isAstDescendant(current, statement));
31736
+ if (containingStatementIndex >= 0) {
31737
+ if ((parent.body ?? []).slice(0, containingStatementIndex).some((statement) => !statementCanCompleteNormally(statement, environment))) return false;
31738
+ }
31739
+ }
31740
+ current = parent;
31741
+ }
31742
+ return true;
31743
+ };
31744
+ const createStateTriggerReachability = ({ analysis, context, effectFunction }) => {
31745
+ const snapshotEnvironment = collectSnapshotEnvironment(analysis, effectFunction, context);
31746
+ if (snapshotEnvironment.refShapes.size === 0) return () => true;
31747
+ const environment = {
31748
+ analysis,
31749
+ context,
31750
+ effectFunction,
31751
+ snapshotEnvironment,
31752
+ substitutions: /* @__PURE__ */ new Map(),
31753
+ visitedSymbolIds: /* @__PURE__ */ new Set(),
31754
+ allowHelperCall: true
31755
+ };
31756
+ return (target) => isReachableUnderSnapshotEnvironment(target, effectFunction, environment);
31757
+ };
31758
+ //#endregion
30655
31759
  //#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
30656
31760
  const getUseStateDeclarator = (ref) => (ref.resolved?.defs ?? []).map((def) => def.node).find((node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "ArrayPattern")) ?? null;
30657
31761
  const isDeclaredWithin = (node, container) => {
@@ -30717,11 +31821,17 @@ const noChainStateUpdates = defineRule({
30717
31821
  if (stateDeps.length === 0) return;
30718
31822
  if (stateDeps.every((ref) => isExternallyDrivenState(analysis, ref))) return;
30719
31823
  const stateDepDeclarators = new Set(stateDeps.map((ref) => getUseStateDeclarator(ref)).filter((declarator) => declarator !== null));
31824
+ const isReachableFromStateTrigger = createStateTriggerReachability({
31825
+ analysis,
31826
+ context,
31827
+ effectFunction: effectFn
31828
+ });
30720
31829
  const domSyncedStateDeclarators = /* @__PURE__ */ new Set();
30721
31830
  for (const ref of effectFnRefs) {
30722
31831
  if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
30723
31832
  const callExpr = getCallExpr(ref);
30724
31833
  if (!callExpr) continue;
31834
+ if (!isReachableFromStateTrigger(callExpr)) continue;
30725
31835
  if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
30726
31836
  const declarator = getUseStateDeclarator(ref);
30727
31837
  if (declarator) domSyncedStateDeclarators.add(declarator);
@@ -30730,6 +31840,7 @@ const noChainStateUpdates = defineRule({
30730
31840
  if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
30731
31841
  const callExpr = getCallExpr(ref);
30732
31842
  if (!callExpr) continue;
31843
+ if (!isReachableFromStateTrigger(callExpr)) continue;
30733
31844
  if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
30734
31845
  const setterDeclarator = getUseStateDeclarator(ref);
30735
31846
  if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
@@ -40929,7 +42040,7 @@ const noPropCallbackInRender = defineRule({
40929
42040
  if (!analysis) return;
40930
42041
  const callee = stripParenExpression(node.callee);
40931
42042
  if (isFunctionLike$1(callee)) return;
40932
- if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
42043
+ if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference, { nativeMethodScopes: context.scopes }))) return;
40933
42044
  context.report({
40934
42045
  node,
40935
42046
  message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
@@ -58893,6 +60004,78 @@ const inferDestructureSourceKey = (bindingIdentifier) => {
58893
60004
  return null;
58894
60005
  };
58895
60006
  const isReactUseHook = (hookName) => hookName === "use";
60007
+ const isReactHookCapabilityValue = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
60008
+ const unwrappedExpression = stripParenExpression(expression);
60009
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
60010
+ const symbol = scopes.symbolFor(unwrappedExpression);
60011
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
60012
+ visitedSymbolIds.add(symbol.id);
60013
+ if (symbol.kind === "import") return isReactImport(symbol) && isReactHookName(getImportedName(symbol.declarationNode) ?? "");
60014
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
60015
+ const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
60016
+ if (destructuredPropertyName) {
60017
+ const namespaceExpression = stripParenExpression(symbol.initializer);
60018
+ return isReactHookName(destructuredPropertyName) && isNodeOfType(namespaceExpression, "Identifier") && isReactNamespaceImport(namespaceExpression, scopes) && !hasPossibleStaticPropertyMutationOrEscape(namespaceExpression, destructuredPropertyName, scopes);
60019
+ }
60020
+ if (symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
60021
+ return isReactHookCapabilityValue(symbol.initializer, scopes, visitedSymbolIds);
60022
+ }
60023
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return false;
60024
+ const propertyName = getStaticPropertyName(unwrappedExpression);
60025
+ if (!propertyName || !isReactHookName(propertyName)) return false;
60026
+ const namespaceExpression = stripParenExpression(unwrappedExpression.object);
60027
+ return isNodeOfType(namespaceExpression, "Identifier") && isReactNamespaceImport(namespaceExpression, scopes) && !hasPossibleStaticPropertyMutationOrEscape(namespaceExpression, propertyName, scopes);
60028
+ };
60029
+ const isReactHookCapabilityComparisonOperand = (expression, scopes) => {
60030
+ const unwrappedExpression = stripParenExpression(expression);
60031
+ if (isReactHookCapabilityValue(unwrappedExpression, scopes)) return true;
60032
+ return isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "typeof" && isReactHookCapabilityValue(unwrappedExpression.argument, scopes);
60033
+ };
60034
+ const isStaticCapabilityComparisonValue = (expression, scopes) => {
60035
+ const unwrappedExpression = stripParenExpression(expression);
60036
+ if (isNodeOfType(unwrappedExpression, "Literal")) return true;
60037
+ return isNodeOfType(unwrappedExpression, "Identifier") && unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression);
60038
+ };
60039
+ const CAPABILITY_COMPARISON_OPERATORS = new Set([
60040
+ "==",
60041
+ "!=",
60042
+ "===",
60043
+ "!=="
60044
+ ]);
60045
+ const isInvariantReactHookCapabilityCondition = (expression, scopes) => {
60046
+ const unwrappedExpression = stripParenExpression(expression);
60047
+ if (isReactHookCapabilityValue(unwrappedExpression, scopes)) return true;
60048
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return isInvariantReactHookCapabilityCondition(unwrappedExpression.argument, scopes);
60049
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return isInvariantReactHookCapabilityCondition(unwrappedExpression.left, scopes) && isInvariantReactHookCapabilityCondition(unwrappedExpression.right, scopes);
60050
+ if (!isNodeOfType(unwrappedExpression, "BinaryExpression") || !CAPABILITY_COMPARISON_OPERATORS.has(unwrappedExpression.operator)) return false;
60051
+ return isReactHookCapabilityComparisonOperand(unwrappedExpression.left, scopes) && isStaticCapabilityComparisonValue(unwrappedExpression.right, scopes) || isReactHookCapabilityComparisonOperand(unwrappedExpression.right, scopes) && isStaticCapabilityComparisonValue(unwrappedExpression.left, scopes);
60052
+ };
60053
+ const statementContainsOwnAbruptCompletion = (statement) => {
60054
+ let doesContainAbruptCompletion = false;
60055
+ walkAst(statement, (child) => {
60056
+ if (child !== statement && isFunctionLike$1(child)) return false;
60057
+ if (isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement")) {
60058
+ doesContainAbruptCompletion = true;
60059
+ return false;
60060
+ }
60061
+ });
60062
+ return doesContainAbruptCompletion;
60063
+ };
60064
+ const isInvariantReactHookCapabilityExit = (statement, scopes) => isNodeOfType(statement, "IfStatement") && statement.alternate === null && statementAlwaysExits(statement.consequent) && isInvariantReactHookCapabilityCondition(statement.test, scopes);
60065
+ const isAfterOnlyInvariantReactHookCapabilityExits = (node, enclosingFunction, scopes) => {
60066
+ if (!isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
60067
+ let containingStatement = node;
60068
+ while (containingStatement.parent && containingStatement.parent !== enclosingFunction.body) {
60069
+ if (isFunctionLike$1(containingStatement.parent)) return false;
60070
+ if (isNodeOfType(containingStatement.parent, "IfStatement") || isNodeOfType(containingStatement.parent, "SwitchStatement") || isNodeOfType(containingStatement.parent, "SwitchCase") || isNodeOfType(containingStatement.parent, "ConditionalExpression") || isNodeOfType(containingStatement.parent, "LogicalExpression")) return false;
60071
+ containingStatement = containingStatement.parent;
60072
+ }
60073
+ if (containingStatement.parent !== enclosingFunction.body) return false;
60074
+ const statementIndex = enclosingFunction.body.body.findIndex((statement) => statement === containingStatement);
60075
+ if (statementIndex <= 0) return false;
60076
+ const bypassingStatements = enclosingFunction.body.body.slice(0, statementIndex).filter((statement) => statementContainsOwnAbruptCompletion(statement));
60077
+ return bypassingStatements.length > 0 && bypassingStatements.every((statement) => isInvariantReactHookCapabilityExit(statement, scopes));
60078
+ };
58896
60079
  const getCallExpressionCalleeName = (callExpression) => {
58897
60080
  const callee = callExpression.callee;
58898
60081
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -59188,7 +60371,7 @@ const rulesOfHooks = defineRule({
59188
60371
  });
59189
60372
  return;
59190
60373
  }
59191
- if (!context.cfg.isUnconditionalFromEntry(node)) context.report({
60374
+ if (!context.cfg.isUnconditionalFromEntry(node) && !isAfterOnlyInvariantReactHookCapabilityExits(node, enclosing.node, context.scopes)) context.report({
59192
60375
  node: node.callee,
59193
60376
  message: buildConditionalMessage(hookName)
59194
60377
  });