oxlint-plugin-react-doctor 0.9.2-dev.c6bdd2d → 0.9.2-dev.d6f02bb

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 +33 -466
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18012,14 +18012,6 @@ const findRenderPhaseComponentOrHook = (node, scopes) => {
18012
18012
  //#region src/plugin/utils/is-event-handler-attribute.ts
18013
18013
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
18014
18014
  //#endregion
18015
- //#region src/plugin/utils/is-early-exit-statement.ts
18016
- const isEarlyExitStatement$1 = (statement) => {
18017
- if (!statement) return false;
18018
- if (statementAlwaysExits$1(statement)) return true;
18019
- if (isNodeOfType(statement, "BlockStatement")) return isEarlyExitStatement$1(statement.body.at(-1));
18020
- return isNodeOfType(statement, "ContinueStatement") || isNodeOfType(statement, "BreakStatement");
18021
- };
18022
- //#endregion
18023
18015
  //#region src/plugin/utils/is-ast-descendant.ts
18024
18016
  /**
18025
18017
  * True when `inner` is `outer` itself or any descendant in the AST
@@ -18238,15 +18230,13 @@ const isSynchronousIteratorCall = (callNode, callbackArgument, scopes) => {
18238
18230
  }
18239
18231
  return Boolean(methodName && EAGER_ITERATOR_METHOD_NAMES.has(methodName) && callNode.arguments[0] === callbackArgument && !isProvablyEmptyEagerCollection(callee.object, scopes) && (methodName === "forEach" ? isProvablyEagerForEachCollection(callee.object, scopes) : isProvablyEagerCollection(callee.object, scopes)));
18240
18232
  };
18241
- const isSynchronousIteratorCallbackCall = (callNode, callbackArgument) => {
18242
- const callee = stripParenExpression(callNode.callee);
18243
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
18244
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments[1] === callbackArgument;
18245
- return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments[0] === callbackArgument;
18246
- };
18247
18233
  const isSynchronousIteratorCallback = (functionNode) => {
18248
18234
  const callNode = functionNode.parent;
18249
- return Boolean(isNodeOfType(callNode, "CallExpression") && isSynchronousIteratorCallbackCall(callNode, functionNode));
18235
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
18236
+ const callee = stripParenExpression(callNode.callee);
18237
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
18238
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments[1] === functionNode;
18239
+ return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments[0] === functionNode;
18250
18240
  };
18251
18241
  //#endregion
18252
18242
  //#region src/plugin/utils/is-within-assignment-target.ts
@@ -18366,35 +18356,11 @@ const resolveEventListenerCaptureValueIdentityKey = (expression, context) => {
18366
18356
  const rightIdentityKey = resolveEventListenerCaptureValueIdentityKey(unwrappedExpression.right, context);
18367
18357
  return leftIdentityKey && rightIdentityKey ? `${unwrappedExpression.type}:${unwrappedExpression.operator}:${leftIdentityKey}:${rightIdentityKey}` : null;
18368
18358
  };
18369
- const resolveReadOnlyEventListenerOptions = (optionsNode, context) => {
18370
- const unwrappedOptions = stripParenExpression(optionsNode);
18371
- if (!isNodeOfType(unwrappedOptions, "Identifier")) return resolveStableValue(unwrappedOptions, context);
18372
- const optionsSymbol = context.scopes.symbolFor(unwrappedOptions);
18373
- const initializer = optionsSymbol?.initializer ? stripParenExpression(optionsSymbol.initializer) : null;
18374
- if (!optionsSymbol || !initializer) return resolveStableValue(unwrappedOptions, context);
18375
- if (!isNodeOfType(initializer, "ObjectExpression")) {
18376
- if (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression")) return null;
18377
- return resolveStableValue(unwrappedOptions, context);
18378
- }
18379
- if (optionsSymbol.kind !== "const") return null;
18380
- return optionsSymbol.references.every((reference) => {
18381
- if (reference.flag !== "read" || isWithinAssignmentTarget(reference.identifier)) return false;
18382
- const referenceRoot = findTransparentExpressionRoot(reference.identifier);
18383
- const callNode = referenceRoot.parent;
18384
- if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments[2] !== referenceRoot) return false;
18385
- const callee = stripParenExpression(callNode.callee);
18386
- if (!isNodeOfType(callee, "MemberExpression")) return false;
18387
- const methodName = getStaticPropertyKeyName(callee);
18388
- return methodName === "addEventListener" || methodName === "removeEventListener";
18389
- }) ? initializer : null;
18390
- };
18391
18359
  const resolveEventListenerCaptureIdentityKey = (optionsNode, context, allowOpaqueOptionsIdentity) => {
18392
- const stableOptionsNode = optionsNode ? resolveReadOnlyEventListenerOptions(optionsNode, context) : null;
18393
- if (optionsNode && !stableOptionsNode) return null;
18394
- const capture = resolveEventListenerCapture(stableOptionsNode, { allowIndeterminateEntries: true });
18360
+ const capture = resolveEventListenerCapture(optionsNode, { allowIndeterminateEntries: true });
18395
18361
  if (capture !== null) return `capture:${String(capture)}`;
18396
- if (!stableOptionsNode) return null;
18397
- const unwrappedOptions = stripParenExpression(stableOptionsNode);
18362
+ if (!optionsNode) return null;
18363
+ const unwrappedOptions = stripParenExpression(optionsNode);
18398
18364
  if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) {
18399
18365
  const optionsKey = allowOpaqueOptionsIdentity ? resolveEventListenerCaptureValueIdentityKey(unwrappedOptions, context) : null;
18400
18366
  return optionsKey ? `options:${optionsKey}` : null;
@@ -18441,8 +18407,12 @@ const doEventListenerCapturesMatch = (registrationOptions, releaseOptions, conte
18441
18407
  return registrationCaptureKey !== null && registrationCaptureKey === resolveEventListenerCaptureIdentityKey(releaseOptions, context, allowOpaqueOptionsIdentity);
18442
18408
  };
18443
18409
  const findAssignedResourceKey = (resourceNode, context) => {
18444
- const currentNode = findTransparentExpressionRoot(resourceNode);
18445
- const parentNode = currentNode.parent;
18410
+ let currentNode = resourceNode;
18411
+ let parentNode = currentNode.parent;
18412
+ while (isNodeOfType(parentNode, "ChainExpression")) {
18413
+ currentNode = parentNode;
18414
+ parentNode = currentNode.parent;
18415
+ }
18446
18416
  if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode) return resolveExpressionKey(parentNode.id, context);
18447
18417
  if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey(parentNode.left, context);
18448
18418
  return null;
@@ -18713,18 +18683,6 @@ const resolveIteratorCollectionKey = (expression, context) => {
18713
18683
  }
18714
18684
  return null;
18715
18685
  };
18716
- const resolveReceiverIteratorCollectionKey = (expression, context) => {
18717
- if (!expression) return null;
18718
- const unwrappedExpression = stripParenExpression(expression);
18719
- if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
18720
- const collectionExpression = findForOfStatementForIteratorExpression(unwrappedExpression, context)?.right;
18721
- if (!collectionExpression) return null;
18722
- const collectionIdentifier = stripParenExpression(collectionExpression);
18723
- if (!isNodeOfType(collectionIdentifier, "Identifier") || !isPrivatePlainConstIdentifier(collectionIdentifier, context)) return null;
18724
- const collectionSymbol = context.scopes.symbolFor(collectionIdentifier);
18725
- const initializer = collectionSymbol?.initializer ? stripParenExpression(collectionSymbol.initializer) : null;
18726
- return collectionSymbol && isNodeOfType(initializer, "ArrayExpression") && hasOnlyReplayableCollectionReferences(collectionIdentifier, context, /* @__PURE__ */ new Set()) ? `symbol:${collectionSymbol.id}` : null;
18727
- };
18728
18686
  const isStableLoopReceiver = (expression, context) => {
18729
18687
  if (!expression) return false;
18730
18688
  const unwrappedExpression = stripParenExpression(expression);
@@ -19495,28 +19453,6 @@ const isFunctionReturnedFromReactHook = (functionNode, context, requireRefProper
19495
19453
  });
19496
19454
  };
19497
19455
  const isFunctionUsedAsReactRef = (functionNode, context) => isFunctionForwardedToReactRef(functionNode, context) || isFunctionReturnedFromReactHook(functionNode, context, true);
19498
- const findCallbackRefReplacementReleaseGuard = (releaseCall, ownerFunction, releaseReceiverKey, registrationReceiverKey, context) => {
19499
- let descendant = releaseCall;
19500
- let ancestor = descendant.parent;
19501
- while (ancestor && ancestor !== ownerFunction) {
19502
- if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && ancestor.alternate === null) {
19503
- const test = stripParenExpression(ancestor.test);
19504
- if (!isNodeOfType(test, "LogicalExpression") || test.operator !== "&&") return null;
19505
- const operands = [stripParenExpression(test.left), stripParenExpression(test.right)];
19506
- const hasLiveReceiverTest = operands.some((operand) => doesTestRequireLiveExpressionKey(operand, releaseReceiverKey, context));
19507
- const hasDifferentReceiverTest = operands.some((operand) => {
19508
- if (!isNodeOfType(operand, "BinaryExpression") || operand.operator !== "!==" && operand.operator !== "!=") return false;
19509
- const leftKey = resolveExpressionKey(operand.left, context);
19510
- const rightKey = resolveExpressionKey(operand.right, context);
19511
- return leftKey === releaseReceiverKey && rightKey === registrationReceiverKey || rightKey === releaseReceiverKey && leftKey === registrationReceiverKey;
19512
- });
19513
- return hasLiveReceiverTest && hasDifferentReceiverTest ? ancestor : null;
19514
- }
19515
- descendant = ancestor;
19516
- ancestor = descendant.parent;
19517
- }
19518
- return null;
19519
- };
19520
19456
  const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
19521
19457
  if (!isNodeOfType(usage.node, "CallExpression")) return false;
19522
19458
  const usageFunction = findEnclosingFunction$1(usage.node);
@@ -19537,7 +19473,7 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
19537
19473
  if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
19538
19474
  if (isNodeOfType(child, "AssignmentExpression") && child.operator === "=" && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === releaseRefSymbol.id && resolveExpressionKey(child.right, context) === registrationReceiverKey && releaseStart !== null && (getRangeStart(child) ?? -1) > releaseStart) matchingOwnershipAssignments.push(child);
19539
19475
  });
19540
- const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? findCallbackRefReplacementReleaseGuard(releaseCall, usageFunction, releaseReceiverKey, registrationReceiverKey, context) ?? releaseCall;
19476
+ const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? releaseCall;
19541
19477
  const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
19542
19478
  return doNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
19543
19479
  };
@@ -19687,21 +19623,14 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
19687
19623
  if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
19688
19624
  if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
19689
19625
  if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
19626
+ if (usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1) return isProvenLegacyMediaQueryListMethodCall(usage.node, "addListener", context) && releaseVerbName === "removeListener" && isProvenLegacyMediaQueryListMethodCall(callNode, "removeListener", context) && usage.receiverKey !== null && resolveStableMediaQueryListenerIdentityKey(callee.object, context) === usage.receiverKey && usage.handlerKey !== null && resolveStableMediaQueryListenerIdentityKey(callNode.arguments?.[0], context) === usage.handlerKey;
19690
19627
  if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
19691
- if (usage.registrationVerbName === "addListener" && releaseVerbName === "removeListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1) {
19692
- if (callNode.arguments?.length !== 1) return false;
19693
- const registrationHandler = resolveStableValue(usage.node.arguments[0], context);
19694
- if (!isProvenLegacyMediaQueryListMethodCall(usage.node, "addListener", context) && !isFunctionLike$1(registrationHandler)) return false;
19695
- }
19696
19628
  if (usage.registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener" && isNodeOfType(usage.node, "CallExpression")) {
19697
19629
  if (!isNodeOfType(stripParenExpression(usage.node.callee), "MemberExpression")) return false;
19698
19630
  if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context, true)) return false;
19699
19631
  }
19700
19632
  if (isNodeOfType(usage.node, "CallExpression") && !hasSafeForEachProjectionCleanup(usage.node, callNode, context)) return false;
19701
- const registrationCallee = isNodeOfType(usage.node, "CallExpression") ? stripParenExpression(usage.node.callee) : null;
19702
- const registrationReceiverCollectionKey = isNodeOfType(registrationCallee, "MemberExpression") ? resolveReceiverIteratorCollectionKey(registrationCallee.object, context) : null;
19703
- const releaseReceiverCollectionKeyForPair = resolveReceiverIteratorCollectionKey(callee.object, context);
19704
- if (!(registrationReceiverCollectionKey !== null && registrationReceiverCollectionKey === releaseReceiverCollectionKeyForPair) && (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey)) return false;
19633
+ if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
19705
19634
  if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
19706
19635
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
19707
19636
  if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
@@ -19738,7 +19667,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
19738
19667
  const usesUnaryListenerSignatureForCalls = isNodeOfType(usage.node, "CallExpression") && usesUnaryListenerSignature(usage.node, callNode);
19739
19668
  const releaseHandler = usesUnaryListenerSignatureForCalls ? callNode.arguments?.[0] : callNode.arguments?.[1];
19740
19669
  if (!releaseHandler) return releaseVerbName === "off";
19741
- const expectedHandlerKey = usesUnaryListenerSignatureForCalls ? usage.handlerKey ?? usage.eventKey : usage.handlerKey;
19670
+ const expectedHandlerKey = usesUnaryListenerSignatureForCalls ? usage.eventKey : usage.handlerKey;
19742
19671
  const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignatureForCalls ? 0 : 1] : null;
19743
19672
  return expectedHandlerKey !== null && resolveResourceIdentityKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
19744
19673
  }
@@ -20212,7 +20141,6 @@ const findRetainedFunctionLeak = (retainedFunction, context, options) => {
20212
20141
  walkAst(body, (child) => {
20213
20142
  if (leak !== null) return false;
20214
20143
  if (isFunctionLike$1(child)) return false;
20215
- if (!isNodeReachableWithinFunction(child, context)) return false;
20216
20144
  if (isSocketConstruction(child) && !doesResourceResultEscape(child, allowReturnedSocketEscape, false, context)) {
20217
20145
  const socketUsage = {
20218
20146
  kind: "socket",
@@ -20465,164 +20393,6 @@ const isInlineRetainedHandlerFunction = (functionNode, context) => {
20465
20393
  const objectParent = objectExpression.parent;
20466
20394
  return (isNodeOfType(objectParent, "CallExpression") && objectParent.arguments.some((argument) => argument === objectExpression) || isNodeOfType(objectParent, "JSXExpressionContainer")) && findRenderPhaseComponentOrHook(parentNode, context.scopes) !== null;
20467
20395
  };
20468
- const readInvocationArgumentValue = (expression, context) => {
20469
- if (!expression) return {
20470
- isDefinitelyUndefined: true,
20471
- truthiness: "falsy"
20472
- };
20473
- const target = stripParenExpression(expression);
20474
- if (isNodeOfType(target, "Literal")) return {
20475
- isDefinitelyUndefined: false,
20476
- truthiness: target.value ? "truthy" : "falsy"
20477
- };
20478
- if (isNodeOfType(target, "Identifier") && target.name === "undefined" && context.scopes.isGlobalReference(target)) return {
20479
- isDefinitelyUndefined: true,
20480
- truthiness: "falsy"
20481
- };
20482
- if (isNodeOfType(target, "UnaryExpression") && target.operator === "void") return {
20483
- isDefinitelyUndefined: true,
20484
- truthiness: "falsy"
20485
- };
20486
- if (isNodeOfType(target, "ArrayExpression") || isNodeOfType(target, "ArrowFunctionExpression") || isNodeOfType(target, "ClassExpression") || isNodeOfType(target, "FunctionExpression") || isNodeOfType(target, "NewExpression") || isNodeOfType(target, "ObjectExpression")) return {
20487
- isDefinitelyUndefined: false,
20488
- truthiness: "truthy"
20489
- };
20490
- return {
20491
- isDefinitelyUndefined: false,
20492
- truthiness: "unknown"
20493
- };
20494
- };
20495
- const readInvocationConditionTruthiness = (expression, parameterValues, context) => {
20496
- const target = stripParenExpression(expression);
20497
- const atomicValue = readInvocationArgumentValue(target, context);
20498
- if (atomicValue.truthiness !== "unknown") return atomicValue.truthiness;
20499
- if (isNodeOfType(target, "Identifier")) {
20500
- const symbol = context.scopes.symbolFor(target);
20501
- return symbol ? parameterValues.get(symbol.id)?.truthiness ?? "unknown" : "unknown";
20502
- }
20503
- if (isNodeOfType(target, "UnaryExpression") && target.operator === "!") {
20504
- const argumentTruthiness = readInvocationConditionTruthiness(target.argument, parameterValues, context);
20505
- return argumentTruthiness === "truthy" ? "falsy" : argumentTruthiness === "falsy" ? "truthy" : "unknown";
20506
- }
20507
- if (isNodeOfType(target, "LogicalExpression")) {
20508
- const leftTruthiness = readInvocationConditionTruthiness(target.left, parameterValues, context);
20509
- const rightTruthiness = readInvocationConditionTruthiness(target.right, parameterValues, context);
20510
- if (target.operator === "&&") {
20511
- if (leftTruthiness === "falsy" || rightTruthiness === "falsy") return "falsy";
20512
- return leftTruthiness === "truthy" && rightTruthiness === "truthy" ? "truthy" : "unknown";
20513
- }
20514
- if (target.operator === "||") {
20515
- if (leftTruthiness === "truthy" || rightTruthiness === "truthy") return "truthy";
20516
- return leftTruthiness === "falsy" && rightTruthiness === "falsy" ? "falsy" : "unknown";
20517
- }
20518
- return "unknown";
20519
- }
20520
- if (isNodeOfType(target, "ConditionalExpression")) {
20521
- const testTruthiness = readInvocationConditionTruthiness(target.test, parameterValues, context);
20522
- if (testTruthiness === "truthy") return readInvocationConditionTruthiness(target.consequent, parameterValues, context);
20523
- if (testTruthiness === "falsy") return readInvocationConditionTruthiness(target.alternate, parameterValues, context);
20524
- const consequentTruthiness = readInvocationConditionTruthiness(target.consequent, parameterValues, context);
20525
- return consequentTruthiness === readInvocationConditionTruthiness(target.alternate, parameterValues, context) ? consequentTruthiness : "unknown";
20526
- }
20527
- if (isNodeOfType(target, "CallExpression") && isNodeOfType(target.callee, "Identifier") && target.callee.name === "Boolean" && context.scopes.isGlobalReference(target.callee) && target.arguments[0] && isAstNode(target.arguments[0])) return readInvocationConditionTruthiness(target.arguments[0], parameterValues, context);
20528
- return "unknown";
20529
- };
20530
- const getInvocationParameterValues = (retainedFunction, invocation, leakNode, context) => {
20531
- const parameterValues = /* @__PURE__ */ new Map();
20532
- if (!isFunctionLike$1(retainedFunction) || !invocation.isDirect) return parameterValues;
20533
- for (const [parameterIndex, parameter] of retainedFunction.params.entries()) {
20534
- const argument = invocation.call.arguments[parameterIndex];
20535
- const argumentExpression = argument && isAstNode(argument) ? argument : null;
20536
- let parameterIdentifier = null;
20537
- let parameterValue = readInvocationArgumentValue(argumentExpression, context);
20538
- if (isNodeOfType(parameter, "Identifier")) parameterIdentifier = parameter;
20539
- else if (isNodeOfType(parameter, "AssignmentPattern") && isNodeOfType(parameter.left, "Identifier")) {
20540
- parameterIdentifier = parameter.left;
20541
- if (parameterValue.isDefinitelyUndefined) parameterValue = readInvocationArgumentValue(parameter.right, context);
20542
- } else if (isNodeOfType(parameter, "RestElement") && isNodeOfType(parameter.argument, "Identifier")) {
20543
- parameterIdentifier = parameter.argument;
20544
- parameterValue = {
20545
- isDefinitelyUndefined: false,
20546
- truthiness: "truthy"
20547
- };
20548
- }
20549
- if (!parameterIdentifier) continue;
20550
- const parameterSymbol = context.scopes.symbolFor(parameterIdentifier);
20551
- if (!parameterSymbol) continue;
20552
- const isWrittenBeforeLeak = parameterSymbol.references.some((reference) => reference.flag !== "read" && reference.identifier.range[0] < leakNode.range[0]);
20553
- parameterValues.set(parameterSymbol.id, isWrittenBeforeLeak ? {
20554
- isDefinitelyUndefined: false,
20555
- truthiness: "unknown"
20556
- } : parameterValue);
20557
- }
20558
- return parameterValues;
20559
- };
20560
- const isLeakPathDisabledForInvocation = (retainedFunction, leakNode, invocation, context) => {
20561
- if (!invocation.isDirect) return false;
20562
- const parameterValues = getInvocationParameterValues(retainedFunction, invocation, leakNode, context);
20563
- let child = leakNode;
20564
- let ancestor = leakNode.parent ?? null;
20565
- while (ancestor && ancestor !== retainedFunction) {
20566
- if (isNodeOfType(ancestor, "BlockStatement")) {
20567
- const childIndex = ancestor.body.findIndex((statement) => statement === child);
20568
- for (const precedingStatement of ancestor.body.slice(0, childIndex)) {
20569
- if (!isNodeOfType(precedingStatement, "IfStatement") || precedingStatement.alternate || !isEarlyExitStatement$1(precedingStatement.consequent)) continue;
20570
- if (readInvocationConditionTruthiness(precedingStatement.test, parameterValues, context) === "truthy") return true;
20571
- }
20572
- }
20573
- let requiredTruthiness = null;
20574
- let condition = null;
20575
- if (isNodeOfType(ancestor, "IfStatement")) {
20576
- condition = ancestor.test;
20577
- requiredTruthiness = ancestor.consequent === child ? "truthy" : "falsy";
20578
- } else if (isNodeOfType(ancestor, "ConditionalExpression")) {
20579
- condition = ancestor.test;
20580
- requiredTruthiness = ancestor.consequent === child ? "truthy" : "falsy";
20581
- } else if (isNodeOfType(ancestor, "LogicalExpression") && ancestor.right === child && ancestor.operator !== "??") {
20582
- condition = ancestor.left;
20583
- requiredTruthiness = ancestor.operator === "&&" ? "truthy" : "falsy";
20584
- } else if ((isNodeOfType(ancestor, "WhileStatement") || isNodeOfType(ancestor, "DoWhileStatement")) && ancestor.body === child) {
20585
- condition = ancestor.test;
20586
- requiredTruthiness = "truthy";
20587
- } else if (isNodeOfType(ancestor, "ForStatement") && ancestor.body === child && ancestor.test) {
20588
- condition = ancestor.test;
20589
- requiredTruthiness = "truthy";
20590
- }
20591
- if (condition && requiredTruthiness) {
20592
- const conditionTruthiness = readInvocationConditionTruthiness(condition, parameterValues, context);
20593
- if (conditionTruthiness !== "unknown" && conditionTruthiness !== requiredTruthiness) return true;
20594
- }
20595
- child = ancestor;
20596
- ancestor = ancestor.parent ?? null;
20597
- }
20598
- return false;
20599
- };
20600
- const getEffectRetainedInvocations = (retainedFunction, context) => {
20601
- if (!isFunctionLike$1(retainedFunction)) return [];
20602
- const componentFunction = findEnclosingFunction$1(retainedFunction);
20603
- if (!componentFunction || !isFunctionLike$1(componentFunction)) return [];
20604
- const invocations = [];
20605
- walkAst(componentFunction.body, (child) => {
20606
- if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactHookCall(child, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return;
20607
- const effectCallback = getEffectCallback(child);
20608
- if (!effectCallback || !isFunctionLike$1(effectCallback)) return;
20609
- walkAst(effectCallback.body, (effectChild) => {
20610
- if (effectChild !== effectCallback.body && isFunctionLike$1(effectChild)) return false;
20611
- if (!isNodeOfType(effectChild, "CallExpression") || !isNodeReachableWithinFunction(effectChild, context)) return;
20612
- const isDirectInvocation = resolveRefOwnedCleanupFunction(effectChild.callee, context) === retainedFunction;
20613
- const isSynchronousIteratorInvocation = effectChild.arguments.some((argument) => isAstNode(argument) && resolveRefOwnedCleanupFunction(argument, context) === retainedFunction && isSynchronousIteratorCallbackCall(effectChild, argument));
20614
- if (isDirectInvocation) invocations.push({
20615
- call: effectChild,
20616
- isDirect: true
20617
- });
20618
- if (isSynchronousIteratorInvocation) invocations.push({
20619
- call: effectChild,
20620
- isDirect: false
20621
- });
20622
- });
20623
- });
20624
- return invocations;
20625
- };
20626
20396
  const effectNeedsCleanup = defineRule({
20627
20397
  id: "effect-needs-cleanup",
20628
20398
  title: "Effect subscription or timer never cleaned up",
@@ -20633,19 +20403,13 @@ const effectNeedsCleanup = defineRule({
20633
20403
  const reportRetainedLeak = (retainedFunction) => {
20634
20404
  const refEffectUsage = getReactRefEffectUsage(retainedFunction, context);
20635
20405
  if (!refEffectUsage && !isPotentiallyReachableFunction(retainedFunction, context)) return;
20636
- const effectInvocations = getEffectRetainedInvocations(retainedFunction, context);
20637
- const isEffectInvoked = effectInvocations.length > 0;
20638
20406
  const leak = findRetainedFunctionLeak(retainedFunction, context, refEffectUsage ? {
20639
20407
  allowReturnedResourceEscape: refEffectUsage.doesEffectOwnEveryResult,
20640
20408
  allowReturnedTimerEscape: false,
20641
20409
  includeOneShotTimers: true,
20642
20410
  requireCallableReturnedResource: true
20643
- } : isEffectInvoked ? {
20644
- allowReturnedTimerEscape: false,
20645
- includeOneShotTimers: true
20646
20411
  } : void 0);
20647
20412
  if (!leak) return;
20648
- if (isEffectInvoked && leak.resourceName === "setTimeout" && (!isNodeReachableWithinFunction(leak.node, context) || isFunctionLike$1(retainedFunction) && retainedFunction.params.length > 0 && !context.cfg.isUnconditionalFromEntry(leak.node) && effectInvocations.every((invocation) => isLeakPathDisabledForInvocation(retainedFunction, leak.node, invocation, context)))) return;
20649
20413
  const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
20650
20414
  context.report({
20651
20415
  node: leak.node,
@@ -45616,6 +45380,14 @@ const noAriaInvalidWithoutDescription = defineRule({
45616
45380
  } })
45617
45381
  });
45618
45382
  //#endregion
45383
+ //#region src/plugin/utils/is-early-exit-statement.ts
45384
+ const isEarlyExitStatement$1 = (statement) => {
45385
+ if (!statement) return false;
45386
+ if (statementAlwaysExits$1(statement)) return true;
45387
+ if (isNodeOfType(statement, "BlockStatement")) return isEarlyExitStatement$1(statement.body.at(-1));
45388
+ return isNodeOfType(statement, "ContinueStatement") || isNodeOfType(statement, "BreakStatement");
45389
+ };
45390
+ //#endregion
45619
45391
  //#region src/plugin/utils/unwrap-negative-guard-form.ts
45620
45392
  const unwrapNegativeGuardForm = (test) => {
45621
45393
  const expression = stripParenExpression(test);
@@ -75011,29 +74783,6 @@ const doesPredicateTruthRequireMatch = (matchCall, predicateFunction) => {
75011
74783
  }
75012
74784
  return !isNegated && predicateFunction.body === child;
75013
74785
  };
75014
- const doesPredicateReturnNormalizedMatch = (matchCall, predicateFunction) => {
75015
- if (!isFunctionLike$1(predicateFunction) || !isNodeOfType(predicateFunction.body, "BlockStatement") || predicateFunction.body.body.length !== 2 || !isNodeOfType(predicateFunction.body.body[0], "VariableDeclaration")) return false;
75016
- const returnStatement = predicateFunction.body.body[1];
75017
- if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
75018
- let negationCount = 0;
75019
- let expression = matchCall;
75020
- let parent = expression.parent ?? null;
75021
- while (parent && parent !== returnStatement) {
75022
- if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
75023
- negationCount += 1;
75024
- expression = parent;
75025
- parent = parent.parent ?? null;
75026
- continue;
75027
- }
75028
- if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) || isNodeOfType(parent, "ChainExpression")) {
75029
- expression = parent;
75030
- parent = parent.parent ?? null;
75031
- continue;
75032
- }
75033
- return false;
75034
- }
75035
- return parent === returnStatement && returnStatement.argument === expression && negationCount % 2 === 0;
75036
- };
75037
74786
  const isStringTypeofGuardForPath = (test, expectedPath) => {
75038
74787
  const target = stripParenExpression(test);
75039
74788
  if (!isNodeOfType(target, "BinaryExpression") || target.operator !== "===") return false;
@@ -75074,26 +74823,6 @@ const pathUsesOptionalAccess = (node) => {
75074
74823
  current = current.object;
75075
74824
  }
75076
74825
  };
75077
- const getNormalizedClassNameRoot = (expression) => {
75078
- const conditional = stripParenExpression(expression);
75079
- if (!isNodeOfType(conditional, "ConditionalExpression")) return null;
75080
- const consequent = stripParenExpression(conditional.consequent);
75081
- const rootIdentifier = getRootIdentifier(consequent);
75082
- if (!rootIdentifier || receiverPathKey(consequent) !== `${rootIdentifier.name}.className`) return null;
75083
- const test = stripParenExpression(conditional.test);
75084
- if (!isNodeOfType(test, "BinaryExpression") || test.operator !== "===") return null;
75085
- const testOperands = [test.left, test.right].map((operand) => stripParenExpression(operand));
75086
- const typeofOperand = testOperands.find((operand) => isNodeOfType(operand, "UnaryExpression"));
75087
- const stringOperand = testOperands.find((operand) => isNodeOfType(operand, "Literal"));
75088
- if (!typeofOperand || !isNodeOfType(typeofOperand, "UnaryExpression") || typeofOperand.operator !== "typeof" || receiverPathKey(typeofOperand.argument) !== `${rootIdentifier.name}.className` || !stringOperand || !isNodeOfType(stringOperand, "Literal") || stringOperand.value !== "string") return null;
75089
- const alternate = stripParenExpression(conditional.alternate);
75090
- if (!isNodeOfType(alternate, "LogicalExpression") || alternate.operator !== "??") return null;
75091
- const fallback = stripParenExpression(alternate.right);
75092
- const attributeCall = stripParenExpression(alternate.left);
75093
- if (!isNodeOfType(fallback, "Literal") || fallback.value !== "" || !isNodeOfType(attributeCall, "CallExpression") || !isNodeOfType(attributeCall.callee, "MemberExpression") || getStaticPropertyName(attributeCall.callee) !== "getAttribute" || receiverPathKey(attributeCall.callee.object) !== rootIdentifier.name) return null;
75094
- const attributeName = attributeCall.arguments[0] ? stripParenExpression(attributeCall.arguments[0]) : null;
75095
- return attributeName && isNodeOfType(attributeName, "Literal") && attributeName.value === "class" ? rootIdentifier : null;
75096
- };
75097
74826
  const isMatchProvenByFindUpUntilPredicate = (assertion, matchReceiver, assertedPattern, context) => {
75098
74827
  const resultIdentifier = getRootIdentifier(matchReceiver);
75099
74828
  const resultPath = receiverPathKey(matchReceiver);
@@ -75102,17 +74831,11 @@ const isMatchProvenByFindUpUntilPredicate = (assertion, matchReceiver, assertedP
75102
74831
  if (!isDirectFinderMatchReturn(assertion) && !isOptionalResultPath) return false;
75103
74832
  const resultSymbol = context.scopes.symbolFor(resultIdentifier);
75104
74833
  const initializer = resultSymbol?.initializer ? stripParenExpression(resultSymbol.initializer) : null;
75105
- if (resultSymbol?.kind !== "const" || !initializer) return false;
75106
- const finderCall = isNodeOfType(initializer, "CallExpression") ? initializer : isNodeOfType(initializer, "ConditionalExpression") ? (() => {
75107
- const alternate = stripParenExpression(initializer.alternate);
75108
- const consequent = stripParenExpression(initializer.consequent);
75109
- return (isNodeOfType(alternate, "Literal") && alternate.value === null || isNodeOfType(alternate, "Identifier") && alternate.name === "undefined" && context.scopes.isGlobalReference(alternate)) && isNodeOfType(consequent, "CallExpression") ? consequent : null;
75110
- })() : null;
75111
- if (!finderCall || !isNodeOfType(finderCall, "CallExpression")) return false;
74834
+ if (resultSymbol?.kind !== "const" || !initializer || !isNodeOfType(initializer, "CallExpression")) return false;
75112
74835
  if (!isOptionalResultPath && !isImmediatelyGuardedFinderResult(assertion, resultSymbol, resultPath, context)) return false;
75113
- const finderCallee = stripParenExpression(finderCall.callee);
74836
+ const finderCallee = stripParenExpression(initializer.callee);
75114
74837
  if (!isNodeOfType(finderCallee, "Identifier") || !(context.scopes.symbolFor(finderCallee)?.kind === "import" && getImportedNameFromModule(assertion, finderCallee.name, CLOUDSCAPE_DOM_MODULE) === "findUpUntil" || finderCallee.name === "findUpUntil" && context.scopes.isGlobalReference(finderCallee))) return false;
75115
- const predicateArgument = finderCall.arguments[1];
74838
+ const predicateArgument = initializer.arguments[1];
75116
74839
  if (!predicateArgument) return false;
75117
74840
  const predicateFunction = resolveExactLocalFunction(predicateArgument, context.scopes);
75118
74841
  if (!predicateFunction || !isFunctionLike$1(predicateFunction)) return false;
@@ -75136,43 +74859,6 @@ const isMatchProvenByFindUpUntilPredicate = (assertion, matchReceiver, assertedP
75136
74859
  });
75137
74860
  return didProveMatch;
75138
74861
  };
75139
- const isMatchProvenByNormalizedFindUpUntilPredicate = (assertion, matchReceiver, assertedPattern, context) => {
75140
- const normalizedReceiver = stripParenExpression(matchReceiver);
75141
- if (!isNodeOfType(normalizedReceiver, "Identifier")) return false;
75142
- const normalizedReceiverSymbol = context.scopes.symbolFor(normalizedReceiver);
75143
- const normalizedReceiverInitializer = normalizedReceiverSymbol?.initializer ? stripParenExpression(normalizedReceiverSymbol.initializer) : null;
75144
- const resultIdentifier = normalizedReceiverInitializer ? getNormalizedClassNameRoot(normalizedReceiverInitializer) : null;
75145
- if (normalizedReceiverSymbol?.kind !== "const" || normalizedReceiverSymbol.references.some((reference) => reference.flag !== "read") || !resultIdentifier) return false;
75146
- const resultSymbol = context.scopes.symbolFor(resultIdentifier);
75147
- const finderCall = resultSymbol?.initializer ? stripParenExpression(resultSymbol.initializer) : null;
75148
- if (resultSymbol?.kind !== "const" || resultSymbol.references.some((reference) => reference.flag !== "read") || !finderCall || !isNodeOfType(finderCall, "CallExpression")) return false;
75149
- const finderCallee = stripParenExpression(finderCall.callee);
75150
- if (!isNodeOfType(finderCallee, "Identifier") || context.scopes.symbolFor(finderCallee)?.kind !== "import" || getImportedNameFromModule(assertion, finderCallee.name, CLOUDSCAPE_DOM_MODULE) !== "findUpUntil") return false;
75151
- if (!isPresenceProvenBeforeNode(assertion, (test) => {
75152
- const expression = stripParenExpression(test);
75153
- return isNodeOfType(expression, "Identifier") && context.scopes.symbolFor(expression)?.id === resultSymbol.id;
75154
- })) return false;
75155
- const predicateArgument = finderCall.arguments[1];
75156
- const predicateFunction = predicateArgument ? resolveExactLocalFunction(predicateArgument, context.scopes) : null;
75157
- if (!predicateFunction || !isFunctionLike$1(predicateFunction) || predicateFunction.async || predicateFunction.generator) return false;
75158
- const predicateParameter = predicateFunction.params[0];
75159
- if (!isNodeOfType(predicateParameter, "Identifier")) return false;
75160
- let didProveNormalizedMatch = false;
75161
- walkAst(predicateFunction.body, (child) => {
75162
- if (didProveNormalizedMatch || isFunctionLike$1(child)) return false;
75163
- if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || getStaticPropertyName(child.callee) !== "match" || !child.arguments[0] || !areRegexPatternsEquivalent(child.arguments[0], assertedPattern, context) || !doesPredicateTruthRequireMatch(child, predicateFunction) && !doesPredicateReturnNormalizedMatch(child, predicateFunction)) return;
75164
- const predicateReceiver = stripParenExpression(child.callee.object);
75165
- if (!isNodeOfType(predicateReceiver, "Identifier")) return;
75166
- const predicateReceiverSymbol = context.scopes.symbolFor(predicateReceiver);
75167
- const predicateReceiverInitializer = predicateReceiverSymbol?.initializer ? stripParenExpression(predicateReceiverSymbol.initializer) : null;
75168
- const predicateRoot = predicateReceiverInitializer ? getNormalizedClassNameRoot(predicateReceiverInitializer) : null;
75169
- if (predicateReceiverSymbol?.kind === "const" && predicateReceiverSymbol.references.every((reference) => reference.flag === "read") && predicateRoot?.name === predicateParameter.name) {
75170
- didProveNormalizedMatch = true;
75171
- return false;
75172
- }
75173
- });
75174
- return didProveNormalizedMatch;
75175
- };
75176
74862
  const scopeProvesFindMatch = (assertion, findReceiver, findPredicate, context) => {
75177
74863
  if (!isStablePredicate(findPredicate, context)) return false;
75178
74864
  return isPresenceProvenBeforeNode(assertion, (test) => testPositivelyContainsCall(test, (call) => {
@@ -75273,123 +74959,6 @@ const isEnsureThenFind = (assertion, findReceiver, findPredicate) => {
75273
74959
  }
75274
74960
  return false;
75275
74961
  };
75276
- const getReceiverRootIdentifier = (node) => {
75277
- let target = stripParenExpression(node);
75278
- while (isNodeOfType(target, "MemberExpression")) target = stripParenExpression(target.object);
75279
- return isNodeOfType(target, "Identifier") ? target : null;
75280
- };
75281
- const getReceiverStatePath = (node) => {
75282
- const target = stripParenExpression(node);
75283
- if (isNodeOfType(target, "Identifier")) return target.name;
75284
- if (!isNodeOfType(target, "MemberExpression")) return null;
75285
- const objectPath = getReceiverStatePath(target.object);
75286
- if (!objectPath) return null;
75287
- return `${objectPath}.${getStaticPropertyName(target) ?? "*"}`;
75288
- };
75289
- const doesReceiverStateChangeBeforeAssertion = (ownerFunction, receiver, startOffset, assertion, context) => {
75290
- const receiverRoot = getReceiverRootIdentifier(receiver);
75291
- const receiverSymbol = receiverRoot ? context.scopes.symbolFor(receiverRoot) : null;
75292
- const receiverPath = getReceiverStatePath(receiver);
75293
- if (!receiverRoot || !receiverSymbol || !receiverPath) return true;
75294
- const receiverAliasPaths = new Map([[receiverSymbol.id, receiverRoot.name]]);
75295
- let didAddAlias = true;
75296
- while (didAddAlias) {
75297
- didAddAlias = false;
75298
- walkAst(ownerFunction, (child) => {
75299
- if (child !== ownerFunction && isFunctionLike$1(child)) return false;
75300
- if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier") || !child.init) return;
75301
- const initializer = stripParenExpression(child.init);
75302
- if (!isNodeOfType(initializer, "Identifier") && !isNodeOfType(initializer, "MemberExpression")) return;
75303
- const initializerRoot = getReceiverRootIdentifier(initializer);
75304
- const initializerSymbol = initializerRoot ? context.scopes.symbolFor(initializerRoot) : null;
75305
- const initializerBasePath = initializerSymbol ? receiverAliasPaths.get(initializerSymbol.id) : null;
75306
- const initializerPath = getReceiverStatePath(initializer);
75307
- if (!initializerRoot || !initializerBasePath || !initializerPath) return;
75308
- const aliasSymbol = context.scopes.symbolFor(child.id);
75309
- if (aliasSymbol && !receiverAliasPaths.has(aliasSymbol.id)) {
75310
- const initializerSuffix = initializerPath.slice(initializerRoot.name.length);
75311
- receiverAliasPaths.set(aliasSymbol.id, `${initializerBasePath}${initializerSuffix}`);
75312
- didAddAlias = true;
75313
- }
75314
- });
75315
- }
75316
- let didChangeReceiverState = false;
75317
- walkAst(ownerFunction, (child) => {
75318
- if (didChangeReceiverState) return false;
75319
- if (child !== ownerFunction && isFunctionLike$1(child)) return false;
75320
- if (child.range[0] <= startOffset || child.range[0] >= assertion.range[0]) return;
75321
- if (isNodeOfType(child, "CallExpression")) {
75322
- didChangeReceiverState = true;
75323
- return false;
75324
- }
75325
- const mutationTarget = isNodeOfType(child, "AssignmentExpression") || isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete" ? stripParenExpression(isNodeOfType(child, "AssignmentExpression") ? child.left : child.argument) : null;
75326
- const mutationRoot = mutationTarget ? getReceiverRootIdentifier(mutationTarget) : null;
75327
- const mutationSymbol = mutationRoot ? context.scopes.symbolFor(mutationRoot) : null;
75328
- const mutationBasePath = mutationSymbol ? receiverAliasPaths.get(mutationSymbol.id) : null;
75329
- const mutationPath = mutationTarget ? getReceiverStatePath(mutationTarget) : null;
75330
- if (mutationRoot && mutationBasePath && mutationPath) {
75331
- const canonicalMutationPath = `${mutationBasePath}${mutationPath.slice(mutationRoot.name.length)}`;
75332
- if (canonicalMutationPath !== receiverPath && !canonicalMutationPath.startsWith(`${receiverPath}.`) && !receiverPath.startsWith(`${canonicalMutationPath}.`)) return;
75333
- didChangeReceiverState = true;
75334
- return false;
75335
- }
75336
- });
75337
- return didChangeReceiverState;
75338
- };
75339
- const isFindProvenByGuardedMaximum = (assertion, findReceiver, findPredicate, context) => {
75340
- const findLookup = findEqualityLookupParts(findPredicate);
75341
- const maximumIdentifier = findLookup ? stripParenExpression(findLookup.comparedValue) : null;
75342
- if (!findLookup || !maximumIdentifier || !isNodeOfType(maximumIdentifier, "Identifier")) return false;
75343
- const maximumSymbol = context.scopes.symbolFor(maximumIdentifier);
75344
- const maximumInitializer = maximumSymbol?.initializer ? stripParenExpression(maximumSymbol.initializer) : null;
75345
- if (maximumSymbol?.kind !== "const" || maximumSymbol.references.some((reference) => reference.flag !== "read") || !maximumInitializer || !isNodeOfType(maximumInitializer, "CallExpression") || !isNodeOfType(maximumInitializer.callee, "MemberExpression") || getStaticPropertyName(maximumInitializer.callee) !== "reduce") return false;
75346
- const filterCall = stripParenExpression(maximumInitializer.callee.object);
75347
- if (!isNodeOfType(filterCall, "CallExpression") || !isNodeOfType(filterCall.callee, "MemberExpression") || getStaticPropertyName(filterCall.callee) !== "filter" || !areNodesLooselyEqual(filterCall.callee.object, findReceiver)) return false;
75348
- const filterPredicate = filterCall.arguments[0] ? stripParenExpression(filterCall.arguments[0]) : null;
75349
- if (!filterPredicate || !isStablePredicate(filterPredicate, context)) return false;
75350
- const reducerArgument = maximumInitializer.arguments[0] ? stripParenExpression(maximumInitializer.arguments[0]) : null;
75351
- const reducerFunction = reducerArgument ? resolveExactLocalFunction(reducerArgument, context.scopes) : null;
75352
- const initialValue = maximumInitializer.arguments[1] ? stripParenExpression(maximumInitializer.arguments[1]) : null;
75353
- if (!reducerFunction || !isFunctionLike$1(reducerFunction) || reducerFunction.async || reducerFunction.generator || !initialValue || !isNodeOfType(initialValue, "Literal") || typeof initialValue.value !== "number") return false;
75354
- const accumulatorParameter = reducerFunction.params[0];
75355
- const itemParameter = reducerFunction.params[1];
75356
- const reducerBody = singleExpressionPredicateBody(reducerFunction);
75357
- if (!isNodeOfType(accumulatorParameter, "Identifier") || !isNodeOfType(itemParameter, "Identifier") || !reducerBody || !isNodeOfType(reducerBody, "CallExpression") || !isNodeOfType(reducerBody.callee, "MemberExpression") || getStaticPropertyName(reducerBody.callee) !== "max") return false;
75358
- const mathReceiver = stripParenExpression(reducerBody.callee.object);
75359
- if (!isNodeOfType(mathReceiver, "Identifier") || mathReceiver.name !== "Math" || !context.scopes.isGlobalReference(mathReceiver) || reducerBody.arguments.length !== 2) return false;
75360
- const accumulatorArgument = reducerBody.arguments.find((argument) => {
75361
- const expression = stripParenExpression(argument);
75362
- return isNodeOfType(expression, "Identifier") && expression.name === accumulatorParameter.name;
75363
- });
75364
- const itemMemberArgument = reducerBody.arguments.find((argument) => {
75365
- const expression = stripParenExpression(argument);
75366
- const rootIdentifier = getRootIdentifier(expression);
75367
- return isNodeOfType(expression, "MemberExpression") && rootIdentifier?.name === itemParameter.name && receiverPathKey(expression)?.slice(itemParameter.name.length + 1) === findLookup.propertyName;
75368
- });
75369
- if (!accumulatorArgument || !itemMemberArgument) return false;
75370
- if (!receiverPathKey(findReceiver)) return false;
75371
- let ownerFunction = assertion.parent ?? null;
75372
- while (ownerFunction && !isFunctionLike$1(ownerFunction)) ownerFunction = ownerFunction.parent ?? null;
75373
- if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
75374
- const maximumEnd = maximumInitializer.range[1];
75375
- if (doesReceiverStateChangeBeforeAssertion(ownerFunction.body, findReceiver, maximumEnd, assertion, context)) return false;
75376
- return isPresenceProvenBeforeNode(assertion, (test) => {
75377
- const comparison = stripParenExpression(test);
75378
- if (!isNodeOfType(comparison, "BinaryExpression")) return false;
75379
- return [[
75380
- comparison.left,
75381
- comparison.right,
75382
- comparison.operator
75383
- ], [
75384
- comparison.right,
75385
- comparison.left,
75386
- comparison.operator === "<" ? ">" : comparison.operator === ">" ? "<" : comparison.operator
75387
- ]].some(([candidateMaximum, candidateInitial, operator]) => {
75388
- const candidateMaximumIdentifier = stripParenExpression(candidateMaximum);
75389
- return operator === ">" && isNodeOfType(candidateMaximumIdentifier, "Identifier") && context.scopes.symbolFor(candidateMaximumIdentifier)?.id === maximumSymbol.id && areNodesLooselyEqual(stripParenExpression(candidateInitial), initialValue);
75390
- });
75391
- });
75392
- };
75393
74962
  const isDefinitelyNonNullishMapValue = (value) => {
75394
74963
  if (!value) return false;
75395
74964
  const expression = stripParenExpression(value);
@@ -75413,15 +74982,15 @@ const unwrapFalseBooleanGuard = (test) => {
75413
74982
  };
75414
74983
  const isEnsureThenMapGet = (assertion, receiver, lookupKey, context) => {
75415
74984
  const stableLookupKey = stripParenExpression(lookupKey);
75416
- const lookupKeyRoot = isNodeOfType(stableLookupKey, "MemberExpression") ? getRootIdentifier(stableLookupKey) : null;
75417
- const lookupKeySymbol = isNodeOfType(stableLookupKey, "Identifier") ? context.scopes.symbolFor(stableLookupKey) : lookupKeyRoot ? context.scopes.symbolFor(lookupKeyRoot) : null;
75418
- if (!isNodeOfType(stableLookupKey, "Identifier") && !isNodeOfType(stableLookupKey, "Literal") && (!isNodeOfType(stableLookupKey, "MemberExpression") || !lookupKeyRoot || lookupKeySymbol?.kind !== "const")) return false;
74985
+ if (!isNodeOfType(stableLookupKey, "Identifier") && !isNodeOfType(stableLookupKey, "Literal")) return false;
75419
74986
  const receiverSymbol = context.scopes.symbolFor(receiver);
75420
74987
  if (!receiverSymbol) return false;
75421
74988
  const receiverMatches = (candidate) => {
75422
74989
  const target = stripParenExpression(candidate);
75423
74990
  return isNodeOfType(target, "Identifier") && context.scopes.symbolFor(target)?.id === receiverSymbol.id;
75424
74991
  };
74992
+ const lookupKeyExpression = stripParenExpression(lookupKey);
74993
+ const lookupKeySymbol = isNodeOfType(lookupKeyExpression, "Identifier") ? context.scopes.symbolFor(lookupKeyExpression) : null;
75425
74994
  let child = assertion;
75426
74995
  let ancestor = assertion.parent ?? null;
75427
74996
  while (ancestor && !isFunctionLike$1(ancestor)) {
@@ -75441,7 +75010,7 @@ const isEnsureThenMapGet = (assertion, receiver, lookupKey, context) => {
75441
75010
  const populationCall = populationCalls[0];
75442
75011
  if (!populationCall) continue;
75443
75012
  const populationCallStart = populationCall.range[0];
75444
- if (Boolean(lookupKeySymbol?.references.some((reference) => reference.flag !== "read" && reference.identifier.range[0] > populationCallStart && reference.identifier.range[0] < assertion.range[0])) || Boolean(lookupKeySymbol && isNodeOfType(stableLookupKey, "MemberExpression") && subtreeWritesSymbol(ancestor, new Set([lookupKeySymbol.id]), context, void 0, assertion))) continue;
75013
+ if (Boolean(lookupKeySymbol?.references.some((reference) => reference.flag !== "read" && reference.identifier.range[0] > populationCallStart && reference.identifier.range[0] < assertion.range[0]))) continue;
75445
75014
  if (receiverSymbol.references.some((reference) => reference.flag !== "read" && reference.identifier.range[0] > populationCallStart && reference.identifier.range[0] < assertion.range[0])) continue;
75446
75015
  if (!indexedRelevantCalls(ancestor).some((laterCall) => {
75447
75016
  if (laterCall.range[0] <= populationCallStart || laterCall.range[0] >= assertion.range[0] || !isNodeOfType(laterCall.callee, "MemberExpression") || !receiverMatches(laterCall.callee.object)) return false;
@@ -75551,7 +75120,6 @@ const noNonNullAssertionOnMaybeUndefinedResult = defineRule({
75551
75120
  const findReceiver = callee.object;
75552
75121
  if (predicate && isExhaustiveLiteralTupleMapping(findReceiver, predicate, context)) return;
75553
75122
  if (predicate && scopeProvesFindMatch(node, findReceiver, predicate, context)) return;
75554
- if (predicate && isFindProvenByGuardedMaximum(node, findReceiver, predicate, context)) return;
75555
75123
  if (predicate && isEnsureThenFind(node, findReceiver, predicate)) return;
75556
75124
  }
75557
75125
  if (methodName === "match") {
@@ -75561,7 +75129,6 @@ const noNonNullAssertionOnMaybeUndefinedResult = defineRule({
75561
75129
  const regexKey = pattern ? regexComparableKey(pattern, context) : null;
75562
75130
  if (pattern && isGuardedAnchoredCharacterMatch(node, matchReceiver, pattern)) return;
75563
75131
  if (pattern && regexKey && isMatchProvenByFindUpUntilPredicate(node, matchReceiver, pattern, context)) return;
75564
- if (pattern && regexKey && isMatchProvenByNormalizedFindUpUntilPredicate(node, matchReceiver, pattern, context)) return;
75565
75132
  if (regexKey && scopeProvesMatchTested(node, regexKey, matchReceiver, context)) return;
75566
75133
  }
75567
75134
  if (methodName === "get") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.9.2-dev.c6bdd2d",
3
+ "version": "0.9.2-dev.d6f02bb",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",