oxlint-plugin-react-doctor 0.7.7-dev.6e5c240 → 0.7.7-dev.760a3b0

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 +581 -49
  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;
@@ -14609,7 +14626,7 @@ const buildAsyncEffectMessage = (hookName) => `\`${hookName}\` was given an asyn
14609
14626
  const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s callback is defined elsewhere, so dependencies can't be checked and stale values can slip through.`;
14610
14627
  const buildUnstableDepMessage = (hookName, depName) => `\`${depName}\` is rebuilt every render, so \`${hookName}\` runs every time.`;
14611
14628
  const buildForwardedUnstableDepMessage = (depName) => `\`${depName}\` is rebuilt every render and reaches a Hook dependency inside this custom Hook.`;
14612
- 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.`;
14613
14630
  const buildRefCleanupMessage = (depName) => `Your cleanup may read the wrong node since the ref \`${depName}\` can change before it runs.`;
14614
14631
  const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render, so the next render reads the old value.`;
14615
14632
  //#endregion
@@ -15369,26 +15386,125 @@ const isExtraDepAllowedForHook = (hookName, node, scopes) => {
15369
15386
  };
15370
15387
  const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
15371
15388
  const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
15372
- const isStableSetterLikeSymbol = (symbol, scopes) => {
15373
- if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
15374
- 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;
15375
15404
  };
15376
- const isConvergingFunctionalUpdater = (node, scopes) => {
15377
- const updater = unwrapExpression$3(node);
15378
- if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
15379
- const previousValueParameter = updater.params?.[0];
15380
- if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
15381
- let returnedExpression = updater.body;
15382
- if (isNodeOfType(updater.body, "BlockStatement")) {
15383
- returnedExpression = null;
15384
- for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
15385
- returnedExpression = statement.argument;
15386
- 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);
15387
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;
15388
15459
  }
15389
- const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
15390
- if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
15391
- const test = unwrapExpression$3(conditional.test);
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));
15468
+ }
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);
15392
15508
  if (!isNodeOfType(test, "BinaryExpression") || ![
15393
15509
  "===",
15394
15510
  "!==",
@@ -15396,38 +15512,50 @@ const isConvergingFunctionalUpdater = (node, scopes) => {
15396
15512
  "!="
15397
15513
  ].includes(test.operator)) return false;
15398
15514
  const isPreviousValue = (expression) => {
15399
- const candidate = unwrapExpression$3(expression);
15400
- 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;
15401
15517
  };
15402
15518
  let comparedValue = null;
15403
15519
  if (isPreviousValue(test.left)) comparedValue = test.right;
15404
- else if (isPreviousValue(test.right)) comparedValue = test.left;
15405
- if (!comparedValue) return false;
15406
- if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
15407
- const isSameComparedValue = (expression) => {
15408
- const candidate = unwrapExpression$3(expression);
15409
- const compared = unwrapExpression$3(comparedValue);
15410
- if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
15411
- if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
15412
- 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;
15413
15527
  };
15414
- return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
15415
- };
15416
- const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
15417
- const parent = identifier.parent;
15418
- if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
15419
- const writtenValue = parent.arguments?.[0];
15420
- return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
15528
+ return test.operator === "===" || test.operator === "==" ? isPreviousValue(candidate.consequent) && isComparedValue(candidate.alternate) : isComparedValue(candidate.consequent) && isPreviousValue(candidate.alternate);
15421
15529
  };
15422
- 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) => {
15423
15551
  let setterName = null;
15424
15552
  const visit = (current) => {
15425
15553
  if (setterName) return;
15426
15554
  if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
15427
15555
  if (isNodeOfType(current, "Identifier")) {
15428
- const symbol = scopes.referenceFor(current)?.resolvedSymbol;
15429
- if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
15430
- setterName = symbol.name;
15556
+ const descriptor = getStateSetterDescriptor(current, scopes);
15557
+ if (descriptor && isProvablyRenderChangingSetterCall(current, scopes)) {
15558
+ setterName = descriptor.setterSymbol.name;
15431
15559
  return;
15432
15560
  }
15433
15561
  }
@@ -15754,7 +15882,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
15754
15882
  }
15755
15883
  if (!depsArgumentRaw) {
15756
15884
  if (callbackToAnalyze && EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) {
15757
- const setterName = findStableSetterReference(callbackToAnalyze, context.scopes);
15885
+ const setterName = findRenderChangingStateSetterName(callbackToAnalyze, context.scopes);
15758
15886
  if (setterName) {
15759
15887
  context.report({
15760
15888
  node: callbackToAnalyze,
@@ -20147,14 +20275,14 @@ const isIndexOfResultUsedAsMembershipTest = (node) => {
20147
20275
  if (isNegativeOneLiteral(otherOperand)) return true;
20148
20276
  return isZeroLiteral(otherOperand) && (parent.operator === ">=" || parent.operator === "<");
20149
20277
  };
20150
- const getTypeAnnotation = (node) => {
20278
+ const getTypeAnnotation$1 = (node) => {
20151
20279
  if (!node || !("typeAnnotation" in node)) return null;
20152
20280
  const annotation = node.typeAnnotation;
20153
20281
  if (!annotation || !isNodeOfType(annotation, "TSTypeAnnotation")) return null;
20154
20282
  return annotation.typeAnnotation;
20155
20283
  };
20156
20284
  const getDeclaredPropertyType = (members, propertyName) => {
20157
- 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);
20158
20286
  return null;
20159
20287
  };
20160
20288
  const getArrayElementType = (typeNode) => {
@@ -20173,7 +20301,7 @@ const getDestructuredDeclaredType = (identifier) => {
20173
20301
  const property = binding.bindingIdentifier.parent;
20174
20302
  const objectPattern = property?.parent;
20175
20303
  if (!isNodeOfType(property, "Property") || !isNodeOfType(property.key, "Identifier") || !isNodeOfType(objectPattern, "ObjectPattern")) return null;
20176
- const propsType = getTypeAnnotation(objectPattern);
20304
+ const propsType = getTypeAnnotation$1(objectPattern);
20177
20305
  if (!propsType) return null;
20178
20306
  if (isNodeOfType(propsType, "TSTypeLiteral")) return getDeclaredPropertyType(propsType.members ?? [], property.key.name);
20179
20307
  if (!isNodeOfType(propsType, "TSTypeReference") || !isNodeOfType(propsType.typeName, "Identifier")) return null;
@@ -20190,7 +20318,7 @@ const getIdentifierDeclaredType = (identifier, visitedBindingIdentifiers = /* @_
20190
20318
  const binding = findVariableInitializer(identifier, identifier.name);
20191
20319
  if (!binding || visitedBindingIdentifiers.has(binding.bindingIdentifier)) return null;
20192
20320
  visitedBindingIdentifiers.add(binding.bindingIdentifier);
20193
- const directType = getTypeAnnotation(binding.bindingIdentifier);
20321
+ const directType = getTypeAnnotation$1(binding.bindingIdentifier);
20194
20322
  if (directType) return directType;
20195
20323
  const initializer = binding.initializer;
20196
20324
  if (isNodeOfType(initializer, "TSAsExpression") || isNodeOfType(initializer, "TSTypeAssertion") || isNodeOfType(initializer, "TSSatisfiesExpression")) return initializer.typeAnnotation;
@@ -27355,6 +27483,211 @@ const computeExternallyDriven = (analysis, declarator) => {
27355
27483
  return hasDeferredCallSite;
27356
27484
  };
27357
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
27358
27691
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
27359
27692
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
27360
27693
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
@@ -27581,7 +27914,132 @@ const isRefCurrent = (ref) => {
27581
27914
  const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isStateSetter(analysis, innerRef));
27582
27915
  const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
27583
27916
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
27584
- 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 = {}) => {
27585
28043
  if (!isPropAlias(analysis, ref)) return false;
27586
28044
  let effectiveNode = ref.identifier;
27587
28045
  let parent = effectiveNode.parent;
@@ -27594,7 +28052,9 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
27594
28052
  if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
27595
28053
  const memberParent = parent.parent;
27596
28054
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
27597
- 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;
27598
28058
  return isWholePropsObjectReference(analysis, ref);
27599
28059
  }
27600
28060
  }
@@ -41065,7 +41525,7 @@ const noPropCallbackInRender = defineRule({
41065
41525
  if (!analysis) return;
41066
41526
  const callee = stripParenExpression(node.callee);
41067
41527
  if (isFunctionLike$1(callee)) return;
41068
- if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
41528
+ if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference, { nativeMethodScopes: context.scopes }))) return;
41069
41529
  context.report({
41070
41530
  node,
41071
41531
  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."
@@ -59029,6 +59489,78 @@ const inferDestructureSourceKey = (bindingIdentifier) => {
59029
59489
  return null;
59030
59490
  };
59031
59491
  const isReactUseHook = (hookName) => hookName === "use";
59492
+ const isReactHookCapabilityValue = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
59493
+ const unwrappedExpression = stripParenExpression(expression);
59494
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
59495
+ const symbol = scopes.symbolFor(unwrappedExpression);
59496
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
59497
+ visitedSymbolIds.add(symbol.id);
59498
+ if (symbol.kind === "import") return isReactImport(symbol) && isReactHookName(getImportedName(symbol.declarationNode) ?? "");
59499
+ if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
59500
+ const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
59501
+ if (destructuredPropertyName) {
59502
+ const namespaceExpression = stripParenExpression(symbol.initializer);
59503
+ return isReactHookName(destructuredPropertyName) && isNodeOfType(namespaceExpression, "Identifier") && isReactNamespaceImport(namespaceExpression, scopes) && !hasPossibleStaticPropertyMutationOrEscape(namespaceExpression, destructuredPropertyName, scopes);
59504
+ }
59505
+ if (symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
59506
+ return isReactHookCapabilityValue(symbol.initializer, scopes, visitedSymbolIds);
59507
+ }
59508
+ if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return false;
59509
+ const propertyName = getStaticPropertyName(unwrappedExpression);
59510
+ if (!propertyName || !isReactHookName(propertyName)) return false;
59511
+ const namespaceExpression = stripParenExpression(unwrappedExpression.object);
59512
+ return isNodeOfType(namespaceExpression, "Identifier") && isReactNamespaceImport(namespaceExpression, scopes) && !hasPossibleStaticPropertyMutationOrEscape(namespaceExpression, propertyName, scopes);
59513
+ };
59514
+ const isReactHookCapabilityComparisonOperand = (expression, scopes) => {
59515
+ const unwrappedExpression = stripParenExpression(expression);
59516
+ if (isReactHookCapabilityValue(unwrappedExpression, scopes)) return true;
59517
+ return isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "typeof" && isReactHookCapabilityValue(unwrappedExpression.argument, scopes);
59518
+ };
59519
+ const isStaticCapabilityComparisonValue = (expression, scopes) => {
59520
+ const unwrappedExpression = stripParenExpression(expression);
59521
+ if (isNodeOfType(unwrappedExpression, "Literal")) return true;
59522
+ return isNodeOfType(unwrappedExpression, "Identifier") && unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression);
59523
+ };
59524
+ const CAPABILITY_COMPARISON_OPERATORS = new Set([
59525
+ "==",
59526
+ "!=",
59527
+ "===",
59528
+ "!=="
59529
+ ]);
59530
+ const isInvariantReactHookCapabilityCondition = (expression, scopes) => {
59531
+ const unwrappedExpression = stripParenExpression(expression);
59532
+ if (isReactHookCapabilityValue(unwrappedExpression, scopes)) return true;
59533
+ if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return isInvariantReactHookCapabilityCondition(unwrappedExpression.argument, scopes);
59534
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return isInvariantReactHookCapabilityCondition(unwrappedExpression.left, scopes) && isInvariantReactHookCapabilityCondition(unwrappedExpression.right, scopes);
59535
+ if (!isNodeOfType(unwrappedExpression, "BinaryExpression") || !CAPABILITY_COMPARISON_OPERATORS.has(unwrappedExpression.operator)) return false;
59536
+ return isReactHookCapabilityComparisonOperand(unwrappedExpression.left, scopes) && isStaticCapabilityComparisonValue(unwrappedExpression.right, scopes) || isReactHookCapabilityComparisonOperand(unwrappedExpression.right, scopes) && isStaticCapabilityComparisonValue(unwrappedExpression.left, scopes);
59537
+ };
59538
+ const statementContainsOwnAbruptCompletion = (statement) => {
59539
+ let doesContainAbruptCompletion = false;
59540
+ walkAst(statement, (child) => {
59541
+ if (child !== statement && isFunctionLike$1(child)) return false;
59542
+ if (isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement")) {
59543
+ doesContainAbruptCompletion = true;
59544
+ return false;
59545
+ }
59546
+ });
59547
+ return doesContainAbruptCompletion;
59548
+ };
59549
+ const isInvariantReactHookCapabilityExit = (statement, scopes) => isNodeOfType(statement, "IfStatement") && statement.alternate === null && statementAlwaysExits(statement.consequent) && isInvariantReactHookCapabilityCondition(statement.test, scopes);
59550
+ const isAfterOnlyInvariantReactHookCapabilityExits = (node, enclosingFunction, scopes) => {
59551
+ if (!isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
59552
+ let containingStatement = node;
59553
+ while (containingStatement.parent && containingStatement.parent !== enclosingFunction.body) {
59554
+ if (isFunctionLike$1(containingStatement.parent)) return false;
59555
+ if (isNodeOfType(containingStatement.parent, "IfStatement") || isNodeOfType(containingStatement.parent, "SwitchStatement") || isNodeOfType(containingStatement.parent, "SwitchCase") || isNodeOfType(containingStatement.parent, "ConditionalExpression") || isNodeOfType(containingStatement.parent, "LogicalExpression")) return false;
59556
+ containingStatement = containingStatement.parent;
59557
+ }
59558
+ if (containingStatement.parent !== enclosingFunction.body) return false;
59559
+ const statementIndex = enclosingFunction.body.body.findIndex((statement) => statement === containingStatement);
59560
+ if (statementIndex <= 0) return false;
59561
+ const bypassingStatements = enclosingFunction.body.body.slice(0, statementIndex).filter((statement) => statementContainsOwnAbruptCompletion(statement));
59562
+ return bypassingStatements.length > 0 && bypassingStatements.every((statement) => isInvariantReactHookCapabilityExit(statement, scopes));
59563
+ };
59032
59564
  const getCallExpressionCalleeName = (callExpression) => {
59033
59565
  const callee = callExpression.callee;
59034
59566
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -59324,7 +59856,7 @@ const rulesOfHooks = defineRule({
59324
59856
  });
59325
59857
  return;
59326
59858
  }
59327
- if (!context.cfg.isUnconditionalFromEntry(node)) context.report({
59859
+ if (!context.cfg.isUnconditionalFromEntry(node) && !isAfterOnlyInvariantReactHookCapabilityExits(node, enclosing.node, context.scopes)) context.report({
59328
59860
  node: node.callee,
59329
59861
  message: buildConditionalMessage(hookName)
59330
59862
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.7-dev.6e5c240",
3
+ "version": "0.7.7-dev.760a3b0",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",