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

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 +44 -180
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11673,25 +11673,6 @@ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(
11673
11673
  //#region src/plugin/utils/is-event-handler-attribute.ts
11674
11674
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
11675
11675
  //#endregion
11676
- //#region src/plugin/utils/is-ast-descendant.ts
11677
- /**
11678
- * True when `inner` is `outer` itself or any descendant in the AST
11679
- * `parent` chain. Walks `inner.parent` upward and stops at either a
11680
- * match (`true`) or the chain's root (`false`).
11681
- *
11682
- * Was duplicated byte-identical in:
11683
- * - exhaustive-deps-symbol-stability.ts
11684
- * - semantic/closure-captures.ts
11685
- */
11686
- const isAstDescendant = (inner, outer) => {
11687
- let current = inner;
11688
- while (current) {
11689
- if (current === outer) return true;
11690
- current = current.parent ?? null;
11691
- }
11692
- return false;
11693
- };
11694
- //#endregion
11695
11676
  //#region src/plugin/utils/react-ref-origin.ts
11696
11677
  const resolveReactRefSymbol = (memberExpression, scopes) => {
11697
11678
  const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
@@ -12341,39 +12322,26 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
12341
12322
  });
12342
12323
  return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
12343
12324
  };
12344
- const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => {
12345
- if (usage.handleKey === null) return null;
12346
- const doesTestRequireLiveHandle = (test) => {
12347
- if (resolveExpressionKey(test, context) === usage.handleKey) return true;
12348
- const unwrappedTest = stripParenExpression(test);
12349
- if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
12350
- const isNullishOperand = (operand) => {
12351
- const unwrappedOperand = stripParenExpression(operand);
12352
- return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
12353
- };
12354
- return resolveExpressionKey(unwrappedTest.left, context) === usage.handleKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === usage.handleKey && isNullishOperand(unwrappedTest.left);
12355
- };
12356
- let ancestor = releaseCall.parent;
12357
- while (ancestor && ancestor !== owner) {
12358
- if (isNodeOfType(ancestor, "IfStatement")) {
12359
- if (ancestor.alternate !== null || !doesTestRequireLiveHandle(ancestor.test) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
12360
- return ancestor;
12361
- }
12362
- ancestor = ancestor.parent;
12363
- }
12364
- return null;
12365
- };
12366
12325
  const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
12367
12326
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
12368
12327
  const functionCfg = context.cfg.cfgFor(callback);
12369
12328
  const usageBlock = functionCfg?.blockOf(usage.node);
12370
12329
  const usageStart = getRangeStart(usage.node);
12371
12330
  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
+ };
12372
12340
  const matchingReleaseAnchors = [];
12373
12341
  walkInsideStatementBlocks(callback.body, (child) => {
12374
12342
  if (!isNodeOfType(child, "CallExpression")) return;
12375
12343
  const releaseStart = getRangeStart(child);
12376
- const handleGuard = findDirectHandleGuardForRelease(child, callback, usage, context);
12344
+ const handleGuard = findHandleGuard(child);
12377
12345
  if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
12378
12346
  if (doesReleaseCallMatchUsage(child, usage, context)) {
12379
12347
  matchingReleaseAnchors.push(handleGuard ?? child);
@@ -12403,12 +12371,12 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
12403
12371
  return didFindUnmountCleanup;
12404
12372
  };
12405
12373
  const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
12406
- const collectBlockingBooleanStates = (expression, blockedExpressionValue, guardNode, context) => {
12374
+ const collectBlockingBooleanStates = (expression, blockedExpressionValue, context) => {
12407
12375
  const unwrappedExpression = stripParenExpression(expression);
12408
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, guardNode, context);
12376
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, context);
12409
12377
  if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
12410
12378
  if (!(unwrappedExpression.operator === "||" && blockedExpressionValue || unwrappedExpression.operator === "&&" && !blockedExpressionValue)) return [];
12411
- return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, guardNode, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, guardNode, context)];
12379
+ return [...collectBlockingBooleanStates(unwrappedExpression.left, blockedExpressionValue, context), ...collectBlockingBooleanStates(unwrappedExpression.right, blockedExpressionValue, context)];
12412
12380
  }
12413
12381
  if (isNodeOfType(unwrappedExpression, "BinaryExpression") && [
12414
12382
  "===",
@@ -12419,59 +12387,35 @@ const collectBlockingBooleanStates = (expression, blockedExpressionValue, guardN
12419
12387
  const leftValue = readStaticBoolean(unwrappedExpression.left);
12420
12388
  const rightValue = readStaticBoolean(unwrappedExpression.right);
12421
12389
  const booleanValue = leftValue ?? rightValue;
12422
- const comparedExpression = leftValue === null ? unwrappedExpression.left : unwrappedExpression.right;
12423
- const comparedKey = resolveExpressionKey(comparedExpression, context);
12390
+ const comparedKey = resolveExpressionKey(leftValue === null ? unwrappedExpression.left : unwrappedExpression.right, context);
12424
12391
  if (booleanValue === null || comparedKey === null) return [];
12425
- const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
12426
12392
  return [{
12427
- bindingIdentifier: isNodeOfType(comparedExpression, "Identifier") ? context.scopes.symbolFor(comparedExpression)?.bindingIdentifier ?? null : null,
12428
- guardNode,
12429
12393
  key: comparedKey,
12430
- value: isEquality === blockedExpressionValue ? booleanValue : !booleanValue
12394
+ value: (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==") === blockedExpressionValue ? booleanValue : !booleanValue
12431
12395
  }];
12432
12396
  }
12433
12397
  const expressionKey = resolveExpressionKey(unwrappedExpression, context);
12434
12398
  return expressionKey === null ? [] : [{
12435
- bindingIdentifier: isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression)?.bindingIdentifier ?? null : null,
12436
- guardNode,
12437
12399
  key: expressionKey,
12438
12400
  value: blockedExpressionValue
12439
12401
  }];
12440
12402
  };
12441
- const canNodeReachLaterNodeWithinFunction = (sourceNode, targetNode, owner, context) => {
12442
- const functionCfg = context.cfg.cfgFor(owner);
12443
- const sourceBlock = functionCfg?.blockOf(sourceNode);
12444
- const targetBlock = functionCfg?.blockOf(targetNode);
12445
- const sourceStart = getRangeStart(sourceNode);
12446
- const targetStart = getRangeStart(targetNode);
12447
- if (!functionCfg || !sourceBlock || !targetBlock || sourceStart === null || targetStart === null) return true;
12448
- if (!isNodeReachableWithinFunction(sourceNode, context)) return false;
12449
- if (sourceBlock === targetBlock) return sourceStart < targetStart;
12450
- const visitedBlocks = new Set([sourceBlock]);
12451
- const pendingBlocks = [sourceBlock];
12452
- while (pendingBlocks.length > 0) {
12453
- const currentBlock = pendingBlocks.pop();
12454
- if (!currentBlock) break;
12455
- for (const edge of currentBlock.successors) {
12456
- if (edge.to === targetBlock) return true;
12457
- if (visitedBlocks.has(edge.to)) continue;
12458
- visitedBlocks.add(edge.to);
12459
- pendingBlocks.push(edge.to);
12460
- }
12461
- }
12462
- return false;
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");
12463
12407
  };
12464
12408
  const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
12465
12409
  if (!isFunctionLike$1(callback) || callback.async) return [];
12466
12410
  const guardStates = [];
12467
12411
  walkAst(callback.body, (child) => {
12468
12412
  if (child !== callback.body && isFunctionLike$1(child)) return false;
12469
- 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));
12413
+ if (isNodeOfType(child, "IfStatement") && isDirectEarlyReturnConsequent(child) && doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], callback, context)) guardStates.push(...collectBlockingBooleanStates(child.test, true, context));
12470
12414
  });
12471
12415
  let descendant = usageNode;
12472
12416
  let ancestor = descendant.parent;
12473
12417
  while (ancestor && ancestor !== callback) {
12474
- if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, ancestor, context));
12418
+ if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant) guardStates.push(...collectBlockingBooleanStates(ancestor.test, false, context));
12475
12419
  descendant = ancestor;
12476
12420
  ancestor = ancestor.parent;
12477
12421
  }
@@ -12480,7 +12424,7 @@ const collectDeferredUsageGuardStates = (callback, usageNode, context) => {
12480
12424
  const cleanupReturnInvalidatesGuard = (cleanupReturn, guardState, context) => {
12481
12425
  if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
12482
12426
  const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
12483
- if (!cleanupFunction || !isFunctionLike$1(cleanupFunction) || cleanupFunction.async || cleanupFunction.generator) return false;
12427
+ if (!cleanupFunction || !isFunctionLike$1(cleanupFunction) || cleanupFunction.async) return false;
12484
12428
  let didInvalidateGuard = false;
12485
12429
  walkAst(cleanupFunction.body, (child) => {
12486
12430
  if (didInvalidateGuard) return false;
@@ -12509,110 +12453,11 @@ const deferredUsageWritesGuardBeforeUsage = (callback, usageNode, guardState, co
12509
12453
  });
12510
12454
  return didWriteGuard;
12511
12455
  };
12512
- const canInterruptionReachUsageThroughCatch = (interruptionNode, usageNode, owner, context) => {
12513
- let descendant = interruptionNode;
12514
- let ancestor = descendant.parent;
12515
- while (ancestor && ancestor !== owner) {
12516
- if (isNodeOfType(ancestor, "TryStatement") && ancestor.block === descendant && ancestor.handler && canNodeReachLaterNodeWithinFunction(ancestor.handler.body, usageNode, owner, context)) return true;
12517
- descendant = ancestor;
12518
- ancestor = ancestor.parent;
12519
- }
12520
- return false;
12521
- };
12522
- const isEffectLocalLifecycleGuard = (callback, guardState, cleanupFunctions, context) => {
12523
- if (!guardState.bindingIdentifier) return false;
12524
- const guardSymbol = context.scopes.symbolFor(guardState.bindingIdentifier);
12525
- if (!guardSymbol || guardSymbol.kind !== "let" && guardSymbol.kind !== "var" || !isNodeOfType(guardSymbol.declarationNode, "VariableDeclarator") || findEnclosingFunction$1(guardSymbol.declarationNode) !== callback) return false;
12526
- return guardSymbol.references.every((reference) => {
12527
- if (!isWithinAssignmentTarget(reference.identifier)) return true;
12528
- const assignmentTarget = findTransparentExpressionRoot(reference.identifier);
12529
- const assignment = assignmentTarget.parent;
12530
- return isNodeOfType(assignment, "AssignmentExpression") && assignment.operator === "=" && assignment.left === assignmentTarget && readStaticBoolean(assignment.right) === guardState.value && cleanupFunctions.includes(findEnclosingFunction$1(assignment) ?? assignment);
12531
- });
12532
- };
12533
- const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, context) => {
12534
- if (!isFunctionLike$1(callback)) return true;
12535
- const guardStart = getRangeStart(guardState.guardNode);
12536
- const usageStart = getRangeStart(usageNode);
12537
- if (guardStart === null || usageStart === null) return true;
12538
- let hasPotentialInterruption = false;
12539
- walkAst(callback.body, (child) => {
12540
- if (hasPotentialInterruption) return false;
12541
- if (child !== callback.body && isFunctionLike$1(child)) return false;
12542
- const childStart = getRangeStart(child);
12543
- if (childStart === null || childStart <= guardStart || childStart >= usageStart) return;
12544
- if (isNodeOfType(child, "CallExpression") || isNodeOfType(child, "AwaitExpression") || isNodeOfType(child, "YieldExpression")) {
12545
- if (canNodeReachLaterNodeWithinFunction(child, usageNode, callback, context) || canInterruptionReachUsageThroughCatch(child, usageNode, callback, context)) {
12546
- hasPotentialInterruption = true;
12547
- return false;
12548
- }
12549
- }
12550
- });
12551
- return hasPotentialInterruption;
12552
- };
12553
12456
  const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
12554
12457
  const usageFunction = findEnclosingFunction$1(usage.node);
12555
12458
  const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
12556
- 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;
12557
- const usageExpression = findTransparentExpressionRoot(usage.node);
12558
- const usageAssignment = usageExpression.parent;
12559
- if (!isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !isNodeOfType(usageAssignment.left, "Identifier")) return false;
12560
- const handleSymbol = context.scopes.symbolFor(usageAssignment.left);
12561
- if (!handleSymbol || handleSymbol.kind !== "let" && handleSymbol.kind !== "var" || !isNodeOfType(handleSymbol.declarationNode, "VariableDeclarator") || findEnclosingFunction$1(handleSymbol.declarationNode) !== callback) return false;
12562
- const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
12563
- if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
12564
- const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
12565
- return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
12566
- });
12567
- if (cleanupFunctions.length !== cleanupReturns.length) return false;
12568
- const globalReleaseProofsByCleanup = /* @__PURE__ */ new Map();
12569
- for (const cleanupFunction of cleanupFunctions) {
12570
- if (!isFunctionLike$1(cleanupFunction)) return false;
12571
- const globalReleaseProofs = [];
12572
- walkAst(cleanupFunction.body, (child) => {
12573
- if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
12574
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && context.scopes.isGlobalReference(child.callee) && doesReleaseCallMatchUsage(child, usage, context)) {
12575
- const handleGuard = findDirectHandleGuardForRelease(child, cleanupFunction, usage, context);
12576
- globalReleaseProofs.push({
12577
- anchor: handleGuard ?? child,
12578
- call: child,
12579
- handleGuard
12580
- });
12581
- }
12582
- });
12583
- if (!doMatchingNodesCoverEveryPathFromFunctionEntry(cleanupFunction, globalReleaseProofs.map((releaseProof) => releaseProof.anchor), context)) return false;
12584
- globalReleaseProofsByCleanup.set(cleanupFunction, globalReleaseProofs);
12585
- }
12586
- const handleAssignments = handleSymbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
12587
- const hasUsageAssignment = handleAssignments.some((handleAssignment) => findTransparentExpressionRoot(handleAssignment.identifier).parent === usageAssignment);
12588
- const hasUnsafeHandleAssignment = handleAssignments.some((handleAssignment) => {
12589
- const assignmentTarget = findTransparentExpressionRoot(handleAssignment.identifier);
12590
- const assignment = assignmentTarget.parent;
12591
- if (assignment === usageAssignment) return false;
12592
- if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== assignmentTarget) return true;
12593
- const assignedValue = stripParenExpression(assignment.right);
12594
- if (!(isNodeOfType(assignedValue, "Literal") && assignedValue.value === null || isNodeOfType(assignedValue, "Identifier") && assignedValue.name === "undefined" && context.scopes.isGlobalReference(assignedValue))) return true;
12595
- const cleanupFunction = findEnclosingFunction$1(assignment);
12596
- const globalReleaseProofs = cleanupFunction ? globalReleaseProofsByCleanup.get(cleanupFunction) : void 0;
12597
- return !(cleanupFunction && globalReleaseProofs && doMatchingNodesCoverEveryPathBeforeUsage(assignment, globalReleaseProofs.map((releaseProof) => releaseProof.handleGuard && isAstDescendant(assignment, releaseProof.handleGuard.consequent) ? releaseProof.call : releaseProof.anchor), cleanupFunction, context));
12598
- });
12599
- if (!hasUsageAssignment || hasUnsafeHandleAssignment) return false;
12600
- let usageAncestor = usage.node.parent;
12601
- while (usageAncestor && usageAncestor !== usageFunction) {
12602
- if (isNodeOfType(usageAncestor, "ForStatement") || isNodeOfType(usageAncestor, "ForInStatement") || isNodeOfType(usageAncestor, "ForOfStatement") || isNodeOfType(usageAncestor, "WhileStatement") || isNodeOfType(usageAncestor, "DoWhileStatement")) return false;
12603
- usageAncestor = usageAncestor.parent;
12604
- }
12605
- let hasPotentialInterruption = false;
12606
- for (const argument of usage.node.arguments ?? []) walkAst(argument, (argumentChild) => {
12607
- if (hasPotentialInterruption) return false;
12608
- if (isFunctionLike$1(argumentChild)) return false;
12609
- if (isNodeOfType(argumentChild, "CallExpression") || isNodeOfType(argumentChild, "AwaitExpression") || isNodeOfType(argumentChild, "YieldExpression")) {
12610
- hasPotentialInterruption = true;
12611
- return false;
12612
- }
12613
- });
12614
- if (hasPotentialInterruption) return false;
12615
- 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)));
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)));
12616
12461
  };
12617
12462
  const effectHasCleanupForUsage = (callback, usage, context) => {
12618
12463
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
@@ -13178,7 +13023,7 @@ const effectNeedsCleanup = defineRule({
13178
13023
  const hookName = getCalleeName$2(node) ?? "effect";
13179
13024
  context.report({
13180
13025
  node,
13181
- 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.`
13026
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in ${hookName} without returning cleanup. Return a cleanup function so it does not leak after unmount.`
13182
13027
  });
13183
13028
  },
13184
13029
  FunctionDeclaration(node) {
@@ -14628,6 +14473,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
14628
14473
  };
14629
14474
  };
14630
14475
  //#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
14631
14495
  //#region src/plugin/rules/react-builtins/exhaustive-deps-sole-writer-guard.ts
14632
14496
  const EQUALITY_BINARY_OPERATORS = new Set(["===", "!=="]);
14633
14497
  const getUseStateSetterSymbol = (stateSymbol, scopes) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.7-dev.1043416",
3
+ "version": "0.7.7-dev.287dff7",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",