oxlint-plugin-react-doctor 0.8.1-dev.1839566 → 0.8.1-dev.26b4a0c

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.
package/dist/index.d.ts CHANGED
@@ -9437,7 +9437,7 @@ declare const EXTERNAL_RULES: readonly [{
9437
9437
  }, {
9438
9438
  readonly key: "react-hooks-js/set-state-in-effect";
9439
9439
  readonly source: "react-compiler";
9440
- readonly severity: "error";
9440
+ readonly severity: "warn";
9441
9441
  }, {
9442
9442
  readonly key: "react-hooks-js/globals";
9443
9443
  readonly source: "react-compiler";
@@ -18748,7 +18748,7 @@ declare const RULES: readonly [{
18748
18748
  }, {
18749
18749
  readonly key: "react-hooks-js/set-state-in-effect";
18750
18750
  readonly source: "react-compiler";
18751
- readonly severity: "error";
18751
+ readonly severity: "warn";
18752
18752
  }, {
18753
18753
  readonly key: "react-hooks-js/globals";
18754
18754
  readonly source: "react-compiler";
package/dist/index.js CHANGED
@@ -13961,6 +13961,18 @@ const isNodeReachableWithinFunction = (node, context) => {
13961
13961
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
13962
13962
  const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
13963
13963
  const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
13964
+ const REPLAY_ENTRY_DROPPING_ARRAY_METHOD_NAMES = new Set([
13965
+ "pop",
13966
+ "shift",
13967
+ "splice",
13968
+ "fill",
13969
+ "copyWithin"
13970
+ ]);
13971
+ const REPLAY_ENTRY_DROPPING_COLLECTION_METHOD_NAMES = new Set([
13972
+ "clear",
13973
+ "delete",
13974
+ "set"
13975
+ ]);
13964
13976
  const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
13965
13977
  const RESOURCE_NOUN_BY_KIND = {
13966
13978
  subscribe: "subscription",
@@ -14001,6 +14013,92 @@ const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE
14001
14013
  }
14002
14014
  return null;
14003
14015
  };
14016
+ const resolveForEachProjection = (expression, context) => {
14017
+ if (!expression) return null;
14018
+ let currentExpression = stripParenExpression(expression);
14019
+ const memberNames = [];
14020
+ while (isNodeOfType(currentExpression, "MemberExpression") && !currentExpression.computed) {
14021
+ if (!isNodeOfType(currentExpression.property, "Identifier")) return null;
14022
+ memberNames.unshift(currentExpression.property.name);
14023
+ currentExpression = stripParenExpression(currentExpression.object);
14024
+ }
14025
+ if (!isNodeOfType(currentExpression, "Identifier")) return null;
14026
+ const symbol = context.scopes.symbolFor(currentExpression);
14027
+ if (!symbol || symbol.kind !== "parameter") return null;
14028
+ let callbackNode = symbol.bindingIdentifier.parent;
14029
+ while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
14030
+ if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
14031
+ const forEachCall = findEnclosingForEachCall(callbackNode);
14032
+ if (!forEachCall) return null;
14033
+ const forEachCallee = stripParenExpression(forEachCall.callee);
14034
+ if (!isNodeOfType(forEachCallee, "MemberExpression")) return null;
14035
+ const collectionKey = resolveExpressionKey(forEachCallee.object, context);
14036
+ if (!collectionKey) return null;
14037
+ const firstParameter = callbackNode.params[0];
14038
+ const bindingProperty = symbol.bindingIdentifier.parent;
14039
+ const propertyName = isNodeOfType(firstParameter, "ObjectPattern") && isNodeOfType(bindingProperty, "Property") && bindingProperty.parent === firstParameter && bindingProperty.value === symbol.bindingIdentifier ? getStaticPropertyKeyName(bindingProperty) : null;
14040
+ const parameterProjection = firstParameter === symbol.bindingIdentifier ? "value" : propertyName;
14041
+ if (!parameterProjection) return null;
14042
+ return {
14043
+ collectionKey,
14044
+ projectionKey: [parameterProjection, ...memberNames].join(".")
14045
+ };
14046
+ };
14047
+ const resolveForEachProjectionKey = (expression, context) => {
14048
+ const projection = resolveForEachProjection(expression, context);
14049
+ return projection ? `forEach:${projection.collectionKey}:${projection.projectionKey}` : null;
14050
+ };
14051
+ const resolveResourceIdentityKey = (expression, context) => resolveForEachProjectionKey(expression, context) ?? resolveExpressionKey(expression, context);
14052
+ const resolveEventListenerCaptureIdentityKey = (optionsNode, context, allowOpaqueOptionsIdentity) => {
14053
+ const capture = resolveEventListenerCapture(optionsNode, { allowIndeterminateEntries: true });
14054
+ if (capture !== null) return `capture:${String(capture)}`;
14055
+ if (!optionsNode) return null;
14056
+ const unwrappedOptions = stripParenExpression(optionsNode);
14057
+ if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) {
14058
+ const optionsKey = allowOpaqueOptionsIdentity ? resolveResourceIdentityKey(unwrappedOptions, context) : null;
14059
+ return optionsKey ? `options:${optionsKey}` : null;
14060
+ }
14061
+ let captureKey = "capture:false";
14062
+ for (const property of unwrappedOptions.properties ?? []) {
14063
+ if (!isNodeOfType(property, "Property")) {
14064
+ captureKey = null;
14065
+ continue;
14066
+ }
14067
+ const propertyName = getStaticPropertyKeyName(property);
14068
+ if (propertyName === null || !property.computed && propertyName === "__proto__") {
14069
+ captureKey = null;
14070
+ continue;
14071
+ }
14072
+ if (propertyName === "capture") {
14073
+ const propertyValueKey = resolveResourceIdentityKey(property.value, context);
14074
+ captureKey = propertyValueKey ? `capture-value:${propertyValueKey}` : null;
14075
+ }
14076
+ }
14077
+ return captureKey;
14078
+ };
14079
+ const resolveEventListenerCaptureProjection = (optionsNode, context) => {
14080
+ if (!optionsNode) return null;
14081
+ const unwrappedOptions = stripParenExpression(optionsNode);
14082
+ if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return resolveForEachProjection(unwrappedOptions, context);
14083
+ let captureProjection = null;
14084
+ for (const property of unwrappedOptions.properties ?? []) {
14085
+ if (!isNodeOfType(property, "Property")) {
14086
+ captureProjection = null;
14087
+ continue;
14088
+ }
14089
+ const propertyName = getStaticPropertyKeyName(property);
14090
+ if (propertyName === null || !property.computed && propertyName === "__proto__") {
14091
+ captureProjection = null;
14092
+ continue;
14093
+ }
14094
+ if (propertyName === "capture") captureProjection = resolveForEachProjection(property.value, context);
14095
+ }
14096
+ return captureProjection;
14097
+ };
14098
+ const doEventListenerCapturesMatch = (registrationOptions, releaseOptions, context, allowOpaqueOptionsIdentity = false) => {
14099
+ const registrationCaptureKey = resolveEventListenerCaptureIdentityKey(registrationOptions, context, allowOpaqueOptionsIdentity);
14100
+ return registrationCaptureKey !== null && registrationCaptureKey === resolveEventListenerCaptureIdentityKey(releaseOptions, context, allowOpaqueOptionsIdentity);
14101
+ };
14004
14102
  const findAssignedResourceKey = (resourceNode, context) => {
14005
14103
  let currentNode = resourceNode;
14006
14104
  let parentNode = currentNode.parent;
@@ -14021,10 +14119,10 @@ const getCallRegistrationDetails = (callNode, context) => {
14021
14119
  handlerKey: null
14022
14120
  };
14023
14121
  return {
14024
- receiverKey: resolveExpressionKey(callee.object, context),
14122
+ receiverKey: resolveResourceIdentityKey(callee.object, context),
14025
14123
  registrationVerbName: callee.property.name,
14026
- eventKey: resolveExpressionKey(callNode.arguments?.[0], context),
14027
- handlerKey: resolveExpressionKey(callNode.arguments?.[1], context)
14124
+ eventKey: resolveResourceIdentityKey(callNode.arguments?.[0], context),
14125
+ handlerKey: resolveResourceIdentityKey(callNode.arguments?.[1], context)
14028
14126
  };
14029
14127
  };
14030
14128
  const findSubscribeLikeUsages = (callback, context) => {
@@ -14449,8 +14547,8 @@ const isSynchronousIteratorCallback = (functionNode) => {
14449
14547
  return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
14450
14548
  };
14451
14549
  const findEnclosingForEachCall = (node) => {
14452
- const callbackNode = findEnclosingFunction$1(node);
14453
- if (!callbackNode) return null;
14550
+ const callbackNode = isFunctionLike$1(node) ? node : findEnclosingFunction$1(node);
14551
+ if (!callbackNode || !isFunctionLike$1(callbackNode) || callbackNode.async || callbackNode.generator) return null;
14454
14552
  const callNode = callbackNode.parent;
14455
14553
  if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
14456
14554
  const callee = stripParenExpression(callNode.callee);
@@ -15099,9 +15197,7 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
15099
15197
  const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
15100
15198
  const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
15101
15199
  if (registrationReceiverKey === null || registrationReceiverKey !== nodeParameterKey || releaseReceiverKey === null || usage.eventKey === null || usage.eventKey !== resolveExpressionKey(releaseCall.arguments?.[0], context) || usage.handlerKey === null || usage.handlerKey !== resolveExpressionKey(releaseCall.arguments?.[1], context)) return false;
15102
- const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15103
- const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
15104
- if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15200
+ if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], releaseCall.arguments?.[2], context)) return false;
15105
15201
  const releaseStart = getRangeStart(releaseCall);
15106
15202
  const matchingOwnershipAssignments = [];
15107
15203
  const usageFunctionBody = usageFunction.body;
@@ -15113,6 +15209,126 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
15113
15209
  const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
15114
15210
  return doMatchingNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
15115
15211
  };
15212
+ const findDirectExhaustiveForEachCleanupFunction = (releaseNode, requiredCollectionKeys, context) => {
15213
+ let currentNode = findTransparentExpressionRoot(releaseNode);
15214
+ const visitedFunctions = /* @__PURE__ */ new Set();
15215
+ const replayedCollectionKeys = /* @__PURE__ */ new Set();
15216
+ while (true) {
15217
+ const ownerFunction = findEnclosingFunction$1(currentNode);
15218
+ if (!ownerFunction || !isFunctionLike$1(ownerFunction) || visitedFunctions.has(ownerFunction)) return null;
15219
+ visitedFunctions.add(ownerFunction);
15220
+ const isDirectConciseBody = ownerFunction.body === currentNode;
15221
+ const statementNode = currentNode.parent;
15222
+ const isDirectBlockStatement = isNodeOfType(ownerFunction.body, "BlockStatement") && isNodeOfType(statementNode, "ExpressionStatement") && statementNode.parent === ownerFunction.body;
15223
+ if (!isDirectConciseBody && !isDirectBlockStatement || !doMatchingNodesCoverEveryPathFromFunctionEntry(ownerFunction, [isDirectBlockStatement ? statementNode : currentNode], context)) return null;
15224
+ const forEachCall = findEnclosingForEachCall(ownerFunction);
15225
+ if (!forEachCall) return replayedCollectionKeys.size === requiredCollectionKeys.size && isReturnedEffectCleanupFunction(ownerFunction, context) ? ownerFunction : null;
15226
+ const forEachCallee = stripParenExpression(forEachCall.callee);
15227
+ if (!isNodeOfType(forEachCallee, "MemberExpression")) return null;
15228
+ const collectionKey = resolveExpressionKey(forEachCallee.object, context);
15229
+ if (!collectionKey || !requiredCollectionKeys.has(collectionKey)) return null;
15230
+ replayedCollectionKeys.add(collectionKey);
15231
+ currentNode = findTransparentExpressionRoot(forEachCall);
15232
+ }
15233
+ };
15234
+ const collectReplayOwnerFunctions = (usageNode) => {
15235
+ const ownerFunctions = /* @__PURE__ */ new Set();
15236
+ let currentNode = usageNode;
15237
+ while (true) {
15238
+ const ownerFunction = findEnclosingFunction$1(currentNode);
15239
+ if (!ownerFunction || !isFunctionLike$1(ownerFunction) || ownerFunctions.has(ownerFunction)) break;
15240
+ ownerFunctions.add(ownerFunction);
15241
+ const forEachCall = findEnclosingForEachCall(ownerFunction);
15242
+ if (!forEachCall) break;
15243
+ currentNode = forEachCall;
15244
+ }
15245
+ return ownerFunctions;
15246
+ };
15247
+ const hasCollectionMutationBeforeRelease = (usageNode, releaseNode, collectionKeys, context) => {
15248
+ const usageStart = getRangeStart(usageNode);
15249
+ const releaseStart = getRangeStart(releaseNode);
15250
+ if (usageStart === null || releaseStart === null) return true;
15251
+ const setupOwnerFunctions = collectReplayOwnerFunctions(usageNode);
15252
+ const cleanupOwnerFunctions = collectReplayOwnerFunctions(releaseNode);
15253
+ let programNode = usageNode;
15254
+ while (programNode.parent) programNode = programNode.parent;
15255
+ let didFindMutation = false;
15256
+ walkAst(programNode, (child) => {
15257
+ if (didFindMutation) return false;
15258
+ const childStart = getRangeStart(child);
15259
+ if (childStart === null) return;
15260
+ const ownerFunction = context.cfg.enclosingFunction(child);
15261
+ if (!ownerFunction) return;
15262
+ const isAfterRegistration = setupOwnerFunctions.has(ownerFunction) && childStart > usageStart;
15263
+ const isBeforeRelease = cleanupOwnerFunctions.has(ownerFunction) && childStart < releaseStart;
15264
+ if (!isAfterRegistration && !isBeforeRelease) return;
15265
+ if (isNodeOfType(child, "AssignmentExpression")) {
15266
+ const assignmentKey = resolveExpressionKey(child.left, context);
15267
+ const assignmentTarget = stripParenExpression(child.left);
15268
+ if (assignmentKey && [...collectionKeys].some((collectionKey) => assignmentKey === collectionKey || assignmentKey === `${collectionKey}.length`) || isNodeOfType(assignmentTarget, "MemberExpression") && assignmentTarget.computed && collectionKeys.has(resolveExpressionKey(assignmentTarget.object, context) ?? "")) {
15269
+ didFindMutation = true;
15270
+ return false;
15271
+ }
15272
+ return;
15273
+ }
15274
+ if (isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
15275
+ const deletedMember = stripParenExpression(child.argument);
15276
+ if (!isNodeOfType(deletedMember, "MemberExpression")) return;
15277
+ if (collectionKeys.has(resolveExpressionKey(deletedMember.object, context) ?? "")) {
15278
+ didFindMutation = true;
15279
+ return false;
15280
+ }
15281
+ return;
15282
+ }
15283
+ if (isNodeOfType(child, "UpdateExpression")) {
15284
+ const updatedKey = resolveExpressionKey(child.argument, context);
15285
+ if (updatedKey && [...collectionKeys].some((collectionKey) => updatedKey === `${collectionKey}.length`)) {
15286
+ didFindMutation = true;
15287
+ return false;
15288
+ }
15289
+ return;
15290
+ }
15291
+ if (!isNodeOfType(child, "CallExpression")) return;
15292
+ const callee = stripParenExpression(child.callee);
15293
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !REPLAY_ENTRY_DROPPING_ARRAY_METHOD_NAMES.has(callee.property.name) && !REPLAY_ENTRY_DROPPING_COLLECTION_METHOD_NAMES.has(callee.property.name) || !collectionKeys.has(resolveExpressionKey(callee.object, context) ?? "")) return;
15294
+ didFindMutation = true;
15295
+ return false;
15296
+ });
15297
+ return didFindMutation;
15298
+ };
15299
+ const usesUnaryListenerSignature = (registrationCall, releaseCall) => getCalleeName$1(registrationCall) === "addListener" && registrationCall.arguments?.length === 1 && releaseCall.arguments?.length === 1;
15300
+ const hasSafeForEachProjectionCleanup = (registrationCall, releaseCall, context) => {
15301
+ const registrationCallee = stripParenExpression(registrationCall.callee);
15302
+ const releaseCallee = stripParenExpression(releaseCall.callee);
15303
+ if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return true;
15304
+ const registrationVerbName = getCalleeName$1(registrationCall);
15305
+ const releaseVerbName = getCalleeName$1(releaseCall);
15306
+ const releaseHandler = releaseCall.arguments?.[usesUnaryListenerSignature(registrationCall, releaseCall) ? 0 : 1];
15307
+ const releaseFunction = findEnclosingFunction$1(releaseCall);
15308
+ const registrationEventKey = resolveResourceIdentityKey(registrationCall.arguments?.[0], context);
15309
+ const releaseEventKey = resolveResourceIdentityKey(releaseCall.arguments?.[0], context);
15310
+ const doesHandlerlessOffReleaseEveryRegistration = releaseVerbName === "off" && !releaseHandler && (releaseCall.arguments?.length === 0 || registrationEventKey !== null && registrationEventKey === releaseEventKey);
15311
+ if (Boolean(releaseFunction && isFunctionLike$1(releaseFunction) && isReturnedEffectCleanupFunction(releaseFunction, context) && doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseCall], context)) && (releaseVerbName !== null && UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName) || doesHandlerlessOffReleaseEveryRegistration)) return true;
15312
+ const projections = [
15313
+ registrationCallee.object,
15314
+ registrationCall.arguments?.[0],
15315
+ registrationCall.arguments?.[1],
15316
+ releaseCallee.object,
15317
+ releaseCall.arguments?.[0],
15318
+ releaseCall.arguments?.[1]
15319
+ ].flatMap((expression) => {
15320
+ const projection = resolveForEachProjection(expression, context);
15321
+ return projection ? [projection] : [];
15322
+ });
15323
+ if (registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener") for (const optionsNode of [registrationCall.arguments?.[2], releaseCall.arguments?.[2]]) {
15324
+ const captureProjection = resolveEventListenerCaptureProjection(optionsNode, context);
15325
+ if (captureProjection) projections.push(captureProjection);
15326
+ }
15327
+ if (projections.length === 0) return true;
15328
+ const collectionKeys = new Set(projections.map((projection) => projection.collectionKey));
15329
+ if (!findDirectExhaustiveForEachCleanupFunction(releaseCall, collectionKeys, context)) return false;
15330
+ return !hasCollectionMutationBeforeRelease(registrationCall, releaseCall, collectionKeys, context);
15331
+ };
15116
15332
  const doesReleaseCallMatchUsage = (node, usage, context) => {
15117
15333
  const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
15118
15334
  if (!isNodeOfType(callNode, "CallExpression")) return false;
@@ -15128,8 +15344,8 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15128
15344
  const releaseVerbName = getReleaseVerbName(callNode);
15129
15345
  if (!releaseVerbName) return false;
15130
15346
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
15131
- const releaseReceiverKey = resolveExpressionKey(callee.object, context);
15132
- const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
15347
+ const releaseReceiverKey = resolveResourceIdentityKey(callee.object, context);
15348
+ const releaseEventKey = resolveResourceIdentityKey(callNode.arguments?.[0], context);
15133
15349
  const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
15134
15350
  const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
15135
15351
  const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
@@ -15140,6 +15356,11 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15140
15356
  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;
15141
15357
  if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
15142
15358
  if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
15359
+ if (usage.registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener" && isNodeOfType(usage.node, "CallExpression")) {
15360
+ if (!isNodeOfType(stripParenExpression(usage.node.callee), "MemberExpression")) return false;
15361
+ if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context, true)) return false;
15362
+ }
15363
+ if (isNodeOfType(usage.node, "CallExpression") && !hasSafeForEachProjectionCleanup(usage.node, callNode, context)) return false;
15143
15364
  if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
15144
15365
  if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
15145
15366
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
@@ -15149,6 +15370,9 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15149
15370
  if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
15150
15371
  if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
15151
15372
  if (!isNodeOfType(usage.node, "CallExpression")) return false;
15373
+ const registrationEventProjectionKey = resolveForEachProjectionKey(usage.node.arguments?.[0], context);
15374
+ const releaseEventProjectionKey = resolveForEachProjectionKey(callNode.arguments?.[0], context);
15375
+ if ((registrationEventProjectionKey !== null || releaseEventProjectionKey !== null) && registrationEventProjectionKey !== releaseEventProjectionKey) return false;
15152
15376
  const usageForOfStatement = findForOfStatementForIteratorExpression(usageEventArgument, context);
15153
15377
  const releaseForOfStatement = findForOfStatementForIteratorExpression(releaseEventArgument, context);
15154
15378
  if (usageForOfStatement === null !== (releaseForOfStatement === null)) return false;
@@ -15162,9 +15386,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15162
15386
  const registrationCallee = stripParenExpression(usage.node.callee);
15163
15387
  if (!isNodeOfType(registrationCallee, "MemberExpression")) return false;
15164
15388
  if (!isStableLoopReceiver(registrationCallee.object, context) || !isStableLoopReceiver(callee.object, context) || registrationHandlerSymbolId === null || registrationHandlerSymbolId !== releaseHandlerSymbolId) return false;
15165
- const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15166
- const releaseCapture = resolveEventListenerCapture(callNode.arguments?.[2], { allowIndeterminateEntries: true });
15167
- if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15389
+ if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context)) return false;
15168
15390
  if (!isDirectExhaustiveForOfRelease(callNode, releaseForOfStatement)) return false;
15169
15391
  }
15170
15392
  }
@@ -15173,27 +15395,28 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
15173
15395
  return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
15174
15396
  }
15175
15397
  if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
15176
- const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
15177
- const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
15398
+ const usesUnaryListenerSignatureForCalls = isNodeOfType(usage.node, "CallExpression") && usesUnaryListenerSignature(usage.node, callNode);
15399
+ const releaseHandler = usesUnaryListenerSignatureForCalls ? callNode.arguments?.[0] : callNode.arguments?.[1];
15178
15400
  if (!releaseHandler) return releaseVerbName === "off";
15179
- const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
15180
- const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
15181
- return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
15401
+ const expectedHandlerKey = usesUnaryListenerSignatureForCalls ? usage.eventKey : usage.handlerKey;
15402
+ const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignatureForCalls ? 0 : 1] : null;
15403
+ return expectedHandlerKey !== null && resolveResourceIdentityKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
15182
15404
  }
15183
15405
  if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
15184
15406
  return true;
15185
15407
  };
15186
15408
  const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
15187
15409
  const isReturnedEffectCleanupFunction = (functionNode, context) => {
15188
- let currentNode = functionNode;
15189
- let parentNode = currentNode.parent;
15190
- while (isNodeOfType(parentNode, "ChainExpression") || isNodeOfType(parentNode, "TSAsExpression") || isNodeOfType(parentNode, "TSNonNullExpression")) {
15191
- currentNode = parentNode;
15192
- parentNode = currentNode.parent;
15193
- }
15194
- const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
15195
- const effectCall = effectCallback?.parent;
15196
- return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes));
15410
+ const effectCallback = findEnclosingFunction$1(functionNode);
15411
+ if (!effectCallback || !isFunctionLike$1(effectCallback)) return false;
15412
+ const effectCall = effectCallback.parent;
15413
+ if (!isNodeOfType(effectCall, "CallExpression") || !isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return false;
15414
+ if (!isNodeOfType(effectCallback.body, "BlockStatement")) return resolveStableValue(effectCallback.body, context) === functionNode;
15415
+ let isReturned = false;
15416
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
15417
+ if (isNodeOfType(child, "ReturnStatement") && child.argument && resolveStableValue(child.argument, context) === functionNode) isReturned = true;
15418
+ });
15419
+ return isReturned;
15197
15420
  };
15198
15421
  const isPotentiallyReachableFunction = (functionNode, context) => {
15199
15422
  if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
@@ -15336,11 +15559,9 @@ const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
15336
15559
  };
15337
15560
  const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
15338
15561
  if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
15339
- const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
15340
15562
  const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
15341
15563
  if (!isNodeOfType(releaseCall, "CallExpression")) return false;
15342
- const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
15343
- if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
15564
+ if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], releaseCall.arguments?.[2], context)) return false;
15344
15565
  const ownerFunction = findEnclosingFunction$1(releaseFunction);
15345
15566
  if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
15346
15567
  const triggerRegistrations = [];
@@ -18810,11 +19031,12 @@ const forbidDomProps = defineRule({
18810
19031
  * carrying their own near-identical inlined implementation.
18811
19032
  */
18812
19033
  const flattenCalleeName = (callee) => {
18813
- if (isNodeOfType(callee, "Identifier")) return callee.name;
18814
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
18815
- const objectName = flattenCalleeName(callee.object);
19034
+ const unwrappedCallee = stripParenExpression(callee);
19035
+ if (isNodeOfType(unwrappedCallee, "Identifier")) return unwrappedCallee.name;
19036
+ if (isNodeOfType(unwrappedCallee, "MemberExpression") && !unwrappedCallee.computed) {
19037
+ const objectName = flattenCalleeName(unwrappedCallee.object);
18816
19038
  if (!objectName) return null;
18817
- if (isNodeOfType(callee.property, "Identifier")) return `${objectName}.${callee.property.name}`;
19039
+ if (isNodeOfType(unwrappedCallee.property, "Identifier")) return `${objectName}.${unwrappedCallee.property.name}`;
18818
19040
  }
18819
19041
  return null;
18820
19042
  };
@@ -24587,9 +24809,14 @@ const collectContextBindings = (programRoot) => {
24587
24809
  }
24588
24810
  return bindings;
24589
24811
  };
24590
- const isCreateContextBindingJsxName = (node, contextBindings) => {
24812
+ const isCreateContextBindingJsxName = (node, contextBindings, scopes) => {
24591
24813
  if (!isNodeOfType(node, "JSXIdentifier")) return false;
24592
- if (!contextBindings.has(node.name)) return false;
24814
+ if (!contextBindings.has(node.name)) {
24815
+ const symbol = scopes.symbolFor(node);
24816
+ if (!symbol || symbol.kind !== "import") return false;
24817
+ const importedName = getImportedName(symbol.declarationNode);
24818
+ return getImportSourceForName(node, node.name)?.toLowerCase().includes("context") === true && (node.name.endsWith("Context") || importedName?.endsWith("Context") === true);
24819
+ }
24593
24820
  const binding = findVariableInitializer(node, node.name);
24594
24821
  if (!binding) return false;
24595
24822
  return binding.scopeOwner.type === "Program";
@@ -24613,7 +24840,7 @@ const jsxNoConstructedContextValues = defineRule({
24613
24840
  if (isTestlikeFile) return;
24614
24841
  const nameNode = node.name;
24615
24842
  const isLegacyProvider = isProviderMemberName(nameNode);
24616
- const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings);
24843
+ const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings, context.scopes);
24617
24844
  if (!isLegacyProvider && !isReact19Shorthand) return;
24618
24845
  if (!isInsideFunctionScope(node)) return;
24619
24846
  for (const attribute of node.attributes) {
@@ -24884,6 +25111,29 @@ const buildSameFileJsxSlotPropRegistry = (program, memoRegistry, scopes) => {
24884
25111
  return registry;
24885
25112
  };
24886
25113
  //#endregion
25114
+ //#region src/plugin/utils/has-custom-memo-comparator.ts
25115
+ const MEMO_CALLEE_NAMES$1 = new Set(["memo", "React.memo"]);
25116
+ const isIdentitySensitiveMemoComparator = (comparatorNode, scopes) => {
25117
+ if (isNodeOfType(comparatorNode, "Identifier")) {
25118
+ if (comparatorNode.name === "undefined") return scopes.isGlobalReference(comparatorNode);
25119
+ return getImportedNameFromModule(comparatorNode, comparatorNode.name, "react-redux") === "shallowEqual";
25120
+ }
25121
+ if (!isNodeOfType(comparatorNode, "MemberExpression") || comparatorNode.computed || !isNodeOfType(comparatorNode.object, "Identifier") || !isNodeOfType(comparatorNode.property, "Identifier") || comparatorNode.property.name !== "shallowEqual") return false;
25122
+ return isNamespaceImportFromModule$1(comparatorNode, comparatorNode.object.name, "react-redux");
25123
+ };
25124
+ const hasCustomMemoComparator = (openingName, scopes) => {
25125
+ if (!openingName || !isNodeOfType(openingName, "JSXIdentifier")) return false;
25126
+ const binding = findVariableInitializer(openingName, openingName.name);
25127
+ if (!binding || !binding.initializer) return false;
25128
+ const initializer = binding.initializer;
25129
+ if (!isNodeOfType(initializer, "CallExpression")) return false;
25130
+ const calleeName = flattenCalleeName(initializer.callee);
25131
+ if (calleeName === null || !MEMO_CALLEE_NAMES$1.has(calleeName)) return false;
25132
+ const comparatorNode = (initializer.arguments ?? [])[1];
25133
+ if (!comparatorNode) return false;
25134
+ return !isIdentitySensitiveMemoComparator(comparatorNode, scopes);
25135
+ };
25136
+ //#endregion
24887
25137
  //#region src/plugin/utils/is-on-intrinsic-html-element.ts
24888
25138
  const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
24889
25139
  const openingElement = attribute.parent;
@@ -25169,6 +25419,7 @@ const jsxNoJsxAsProp = defineRule({
25169
25419
  const parentJsxOpening = node.parent;
25170
25420
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
25171
25421
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
25422
+ if (hasCustomMemoComparator(openingName, context.scopes)) return;
25172
25423
  const openingSymbol = openingName && isNodeOfType(openingName, "JSXIdentifier") ? context.scopes.symbolFor(openingName) : null;
25173
25424
  if (openingSymbol && isNodeOfType(node.name, "JSXIdentifier") && jsxSlotPropRegistry?.get(openingSymbol.id)?.has(node.name.name)) return;
25174
25425
  if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
@@ -25188,19 +25439,6 @@ const jsxNoJsxAsProp = defineRule({
25188
25439
  }
25189
25440
  });
25190
25441
  //#endregion
25191
- //#region src/plugin/utils/has-custom-memo-comparator.ts
25192
- const MEMO_CALLEE_NAMES = new Set(["memo", "React.memo"]);
25193
- const hasCustomMemoComparator = (openingName) => {
25194
- if (!openingName || !isNodeOfType(openingName, "JSXIdentifier")) return false;
25195
- const binding = findVariableInitializer(openingName, openingName.name);
25196
- if (!binding || !binding.initializer) return false;
25197
- const initializer = binding.initializer;
25198
- if (!isNodeOfType(initializer, "CallExpression")) return false;
25199
- const calleeName = flattenCalleeName(initializer.callee);
25200
- if (calleeName === null || !MEMO_CALLEE_NAMES.has(calleeName)) return false;
25201
- return (initializer.arguments ?? []).length >= 2;
25202
- };
25203
- //#endregion
25204
25442
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop-tables.ts
25205
25443
  const DATA_ARRAY_PROP_NAMES = new Set([
25206
25444
  "data",
@@ -25559,7 +25797,7 @@ const jsxNoNewArrayAsProp = defineRule({
25559
25797
  const parentJsxOpening = node.parent;
25560
25798
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
25561
25799
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
25562
- if (hasCustomMemoComparator(openingName)) return;
25800
+ if (hasCustomMemoComparator(openingName, context.scopes)) return;
25563
25801
  if (isNodeOfType(node.name, "JSXIdentifier") && isDataArrayPropName(node.name.name)) return;
25564
25802
  if (!isInsideFunctionScope(node)) return;
25565
25803
  const value = node.value;
@@ -26023,6 +26261,7 @@ const jsxNoNewFunctionAsProp = defineRule({
26023
26261
  const parentJsxOpening = node.parent;
26024
26262
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
26025
26263
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
26264
+ if (hasCustomMemoComparator(openingName, context.scopes)) return;
26026
26265
  if (isNodeOfType(node.name, "JSXIdentifier") && isOneShotHandlerName(node.name.name)) return;
26027
26266
  if (!isInsideFunctionScope(node)) return;
26028
26267
  const value = node.value;
@@ -26330,7 +26569,7 @@ const jsxNoNewObjectAsProp = defineRule({
26330
26569
  const parentJsxOpening = node.parent;
26331
26570
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
26332
26571
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
26333
- if (hasCustomMemoComparator(openingName)) return;
26572
+ if (hasCustomMemoComparator(openingName, context.scopes)) return;
26334
26573
  if (!isInsideFunctionScope(node)) return;
26335
26574
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
26336
26575
  if (ALWAYS_FRESH_OBJECT_PROPS.has(node.name.name)) return;
@@ -29362,6 +29601,14 @@ const nextjsNoImgElement = defineRule({
29362
29601
  });
29363
29602
  //#endregion
29364
29603
  //#region src/plugin/rules/nextjs/nextjs-no-native-script.ts
29604
+ const hasEnabledBooleanAttribute = (attributes, attributeName) => {
29605
+ const attribute = findJsxAttribute(attributes, attributeName);
29606
+ if (!attribute) return false;
29607
+ if (attribute.value === null) return true;
29608
+ if (isNodeOfType(attribute.value, "Literal")) return true;
29609
+ if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return false;
29610
+ return isNodeOfType(attribute.value.expression, "Literal") && Boolean(attribute.value.expression.value);
29611
+ };
29365
29612
  const nextjsNoNativeScript = defineRule({
29366
29613
  id: "nextjs-no-native-script",
29367
29614
  title: "Plain script can block Next.js rendering",
@@ -29374,6 +29621,9 @@ const nextjsNoNativeScript = defineRule({
29374
29621
  const typeAttribute = findJsxAttribute(node.attributes ?? [], "type");
29375
29622
  const typeValue = isNodeOfType(typeAttribute?.value, "Literal") ? typeAttribute.value.value : null;
29376
29623
  if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
29624
+ if (hasEnabledBooleanAttribute(node.attributes ?? [], "async")) return;
29625
+ if (hasEnabledBooleanAttribute(node.attributes ?? [], "defer")) return;
29626
+ if (typeValue === "module") return;
29377
29627
  const hasSrcAttribute = Boolean(findJsxAttribute(node.attributes ?? [], "src"));
29378
29628
  if (Boolean(findJsxAttribute(node.attributes ?? [], "dangerouslySetInnerHTML")) && !hasSrcAttribute) return;
29379
29629
  context.report({
@@ -38911,15 +39161,16 @@ const callsOpaqueExternalSetter = (analysisFunctions, setterToStateName) => {
38911
39161
  });
38912
39162
  return didFindOpaqueSetterCall;
38913
39163
  };
38914
- const isReactRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && (isReactApiCall(expression, "useRef", scopes, {
39164
+ const isReactUseRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && isReactApiCall(expression, "useRef", scopes, {
38915
39165
  allowGlobalReactNamespace: true,
38916
39166
  allowUnboundBareCalls: true,
38917
39167
  resolveNamedAliases: true
38918
- }) || isReactApiCall(expression, "createRef", scopes, {
39168
+ });
39169
+ const isReactRefCall = (expression, scopes) => isReactUseRefCall(expression, scopes) || isNodeOfType(expression, "CallExpression") && isReactApiCall(expression, "createRef", scopes, {
38919
39170
  allowGlobalReactNamespace: true,
38920
39171
  allowUnboundBareCalls: true,
38921
39172
  resolveNamedAliases: true
38922
- }));
39173
+ });
38923
39174
  const getDirectReactRefSymbol = (rawExpression, scopes) => {
38924
39175
  const expression = stripParenExpression(rawExpression);
38925
39176
  if (!isNodeOfType(expression, "Identifier")) return null;
@@ -38957,7 +39208,18 @@ const isIntrinsicRefCallbackParameter = (expression, scopes) => {
38957
39208
  const rawFirstParameter = callback.params?.[0];
38958
39209
  const firstParameter = isNodeOfType(rawFirstParameter, "AssignmentPattern") ? rawFirstParameter.left : rawFirstParameter;
38959
39210
  const symbol = scopes.symbolFor(identifier);
38960
- return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter);
39211
+ return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter && symbol.references.every((reference) => {
39212
+ if (reference.flag !== "read") return false;
39213
+ let referenceRoot = reference.identifier;
39214
+ while (referenceRoot.parent) {
39215
+ const parent = referenceRoot.parent;
39216
+ if (isNodeOfType(parent, "AssignmentExpression")) return parent.left !== referenceRoot;
39217
+ if (isNodeOfType(parent, "UpdateExpression")) return parent.argument !== referenceRoot;
39218
+ if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left !== referenceRoot;
39219
+ referenceRoot = parent;
39220
+ }
39221
+ return true;
39222
+ }));
38961
39223
  };
38962
39224
  const getDirectReactRefCall = (symbol, scopes) => {
38963
39225
  const initializer = getDirectUnreassignedInitializer(symbol);
@@ -38965,15 +39227,38 @@ const getDirectReactRefCall = (symbol, scopes) => {
38965
39227
  const expression = stripParenExpression(initializer);
38966
39228
  return isNodeOfType(expression, "CallExpression") && isReactRefCall(expression, scopes) ? expression : null;
38967
39229
  };
39230
+ const isEmptyGlobalMapConstruction = (rawExpression, scopes) => {
39231
+ const expression = stripParenExpression(rawExpression);
39232
+ return isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "Map" && scopes.isGlobalReference(expression.callee) && expression.arguments.length === 0;
39233
+ };
39234
+ const hasEmptyRefSentinelInitializer = (refCall, scopes) => {
39235
+ if (!isReactUseRefCall(refCall, scopes) || refCall.arguments.length > 1) return false;
39236
+ const [initialValue] = refCall.arguments;
39237
+ return !initialValue || isNodeOfType(initialValue, "Literal") && initialValue.value === null || isNodeOfType(initialValue, "Identifier") && initialValue.name === "undefined" && scopes.isGlobalReference(initialValue);
39238
+ };
39239
+ const isDirectLazyEmptyMapInitialization = (currentExpression, symbol, scopes) => {
39240
+ const assignment = currentExpression.parent;
39241
+ if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== currentExpression || assignment.operator !== "??=" || !isEmptyGlobalMapConstruction(assignment.right, scopes)) return false;
39242
+ const refOwner = findEnclosingFunction$1(symbol.bindingIdentifier);
39243
+ return refOwner !== null && findEnclosingFunction$1(assignment) === refOwner;
39244
+ };
38968
39245
  const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
38969
- const initialValue = getDirectReactRefCall(symbol, scopes)?.arguments?.[0];
38970
- if (!initialValue || !isNodeOfType(initialValue, "NewExpression") || !isNodeOfType(initialValue.callee, "Identifier") || initialValue.callee.name !== "Map" || !scopes.isGlobalReference(initialValue.callee) || initialValue.arguments.length !== 0) return false;
39246
+ const refCall = getDirectReactRefCall(symbol, scopes);
39247
+ const initialValue = refCall?.arguments[0];
39248
+ const hasDirectEmptyMapInitializer = Boolean(initialValue && isEmptyGlobalMapConstruction(initialValue, scopes));
39249
+ const hasLazyEmptyMapInitializer = Boolean(refCall && hasEmptyRefSentinelInitializer(refCall, scopes));
39250
+ if (!hasDirectEmptyMapInitializer && !hasLazyEmptyMapInitializer) return false;
38971
39251
  let intrinsicValueWriteCount = 0;
39252
+ let lazyEmptyMapInitializationCount = 0;
38972
39253
  for (const reference of symbol.references) {
38973
39254
  const identifier = findTransparentExpressionRoot(reference.identifier);
38974
39255
  const currentMember = identifier.parent;
38975
39256
  if (!isNodeOfType(currentMember, "MemberExpression") || currentMember.object !== identifier || getStaticPropertyName(currentMember) !== "current") return false;
38976
39257
  const currentExpression = findTransparentExpressionRoot(currentMember);
39258
+ if (hasLazyEmptyMapInitializer && isDirectLazyEmptyMapInitialization(currentExpression, symbol, scopes)) {
39259
+ lazyEmptyMapInitializationCount += 1;
39260
+ continue;
39261
+ }
38977
39262
  const methodMember = currentExpression.parent;
38978
39263
  if (!isNodeOfType(methodMember, "MemberExpression") || methodMember.object !== currentExpression) return false;
38979
39264
  const methodName = getStaticPropertyName(methodMember);
@@ -38986,7 +39271,7 @@ const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
38986
39271
  if (!storedValue || isNodeOfType(storedValue, "SpreadElement") || !isIntrinsicRefCallbackParameter(storedValue, scopes)) return false;
38987
39272
  intrinsicValueWriteCount += 1;
38988
39273
  }
38989
- return intrinsicValueWriteCount > 0;
39274
+ return intrinsicValueWriteCount > 0 && (hasDirectEmptyMapInitializer || lazyEmptyMapInitializationCount === 1);
38990
39275
  };
38991
39276
  const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollectionValue = false, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
38992
39277
  const expression = stripParenExpression(rawExpression);
@@ -39490,7 +39775,7 @@ const noEffectEventInDeps = defineRule({
39490
39775
  title: "Effect Event listed in effect deps",
39491
39776
  tags: ["test-noise"],
39492
39777
  severity: "error",
39493
- recommendation: "Call the useEffectEvent function inside the effect body and don't list it in the deps. It changes on every render on purpose.",
39778
+ recommendation: "Call the useEffectEvent function inside the effect body and don't list it in the deps. It's non-reactive on purpose, so it must be omitted.",
39494
39779
  create: (context) => {
39495
39780
  const componentBindings = createComponentBindingStackTracker({ onVariableDeclarator: (declaratorNode) => {
39496
39781
  if (!isNodeOfType(declaratorNode, "VariableDeclarator")) return;
@@ -39512,7 +39797,7 @@ const noEffectEventInDeps = defineRule({
39512
39797
  if (!isNodeOfType(element, "Identifier")) continue;
39513
39798
  if (componentBindings.isBoundName(element.name)) context.report({
39514
39799
  node: element,
39515
- message: `Listing "${element.name}" in the deps re-runs your effect every render & defeats useEffectEvent.`
39800
+ message: `Listing "${element.name}" in the deps defeats useEffectEvent — Effect Events are non-reactive and must be omitted from deps.`
39516
39801
  });
39517
39802
  }
39518
39803
  }
@@ -39555,6 +39840,7 @@ const noEffectWithFreshDeps = defineRule({
39555
39840
  title: "Effect dependency recreated every render",
39556
39841
  severity: "error",
39557
39842
  category: "State & Effects",
39843
+ disabledWhen: ["react-compiler"],
39558
39844
  recommendation: "Move the value inside the hook body and depend on its simple inputs instead, or wrap it in useMemo / useCallback so it stays the same between renders.",
39559
39845
  create: (context) => ({ CallExpression(node) {
39560
39846
  for (const finding of findForwardedFreshHookDependencies(node, context, EFFECT_HOOK_NAMES$1)) context.report({
@@ -42495,14 +42781,13 @@ const noInlineExhaustiveStyle = defineRule({
42495
42781
  });
42496
42782
  //#endregion
42497
42783
  //#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
42498
- const isMemoCall = (node) => {
42784
+ const MEMO_CALLEE_NAMES = new Set(["memo", "React.memo"]);
42785
+ const isMemoCall = (node) => isNodeOfType(node, "CallExpression") && MEMO_CALLEE_NAMES.has(flattenCalleeName(node.callee) ?? "");
42786
+ const hasCustomComparator = (node, scopes) => {
42499
42787
  if (!isNodeOfType(node, "CallExpression")) return false;
42500
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "memo") return true;
42501
- if (isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "React" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "memo") return true;
42502
- return false;
42788
+ const comparator = node.arguments?.[1];
42789
+ return comparator ? !isIdentitySensitiveMemoComparator(comparator, scopes) : false;
42503
42790
  };
42504
- const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
42505
- const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
42506
42791
  const isInlineReference = (node, scopes) => {
42507
42792
  const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
42508
42793
  if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
@@ -42516,16 +42801,17 @@ const noInlinePropOnMemoComponent = defineRule({
42516
42801
  title: "Inline prop defeats memo()",
42517
42802
  tags: ["test-noise"],
42518
42803
  severity: "warn",
42804
+ disabledWhen: ["react-compiler"],
42519
42805
  recommendation: "Move the inline `() => ...` / `[]` / `{}` to a stable value with useMemo, useCallback, or module scope, so the memoized child stops redrawing on every parent render",
42520
42806
  create: (context) => {
42521
42807
  const memoizedComponentNames = /* @__PURE__ */ new Set();
42522
42808
  return {
42523
42809
  VariableDeclarator(node) {
42524
42810
  if (!isNodeOfType(node.id, "Identifier") || !node.init) return;
42525
- if (isMemoCall(node.init) && !hasCustomComparator(node.init)) memoizedComponentNames.add(node.id.name);
42811
+ if (isMemoCall(node.init) && !hasCustomComparator(node.init, context.scopes)) memoizedComponentNames.add(node.id.name);
42526
42812
  },
42527
42813
  ExportDefaultDeclaration(node) {
42528
- if (node.declaration && isNodeOfType(node.declaration, "CallExpression") && isMemoCall(node.declaration) && !hasCustomComparator(node.declaration)) {
42814
+ if (node.declaration && isNodeOfType(node.declaration, "CallExpression") && isMemoCall(node.declaration) && !hasCustomComparator(node.declaration, context.scopes)) {
42529
42815
  const innerArgument = node.declaration.arguments?.[0];
42530
42816
  if (isNodeOfType(innerArgument, "Identifier")) memoizedComponentNames.add(innerArgument.name);
42531
42817
  }
@@ -55380,6 +55666,7 @@ const preferModuleScopePureFunction = defineRule({
55380
55666
  tags: ["test-noise"],
55381
55667
  severity: "warn",
55382
55668
  category: "Architecture",
55669
+ disabledWhen: ["react-compiler"],
55383
55670
  recommendation: "Move the function above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted work.",
55384
55671
  create: (context) => {
55385
55672
  const report = (functionNode, name, componentName) => {
@@ -56021,10 +56308,10 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
56021
56308
  const preferUseEffectEvent = defineRule({
56022
56309
  id: "prefer-use-effect-event",
56023
56310
  title: "Effect re-subscribes on a changing callback",
56024
- requires: ["react:19"],
56311
+ requires: ["react:19.2"],
56025
56312
  tags: ["test-noise"],
56026
56313
  severity: "warn",
56027
- recommendation: "Wrap the callback with `useEffectEvent(callback)` (React 19+) and call it inside the sub-handler. An Effect Event always sees the latest props and state but isn't a dependency, so the effect won't re-subscribe every time the parent redraws. See https://react.dev/reference/react/useEffectEvent",
56314
+ recommendation: "Wrap the callback with `useEffectEvent(callback)` (React 19.2+) and call it inside the sub-handler. An Effect Event always sees the latest props and state but isn't a dependency, so the effect won't re-subscribe every time the parent redraws. See https://react.dev/reference/react/useEffectEvent",
56028
56315
  create: (context) => {
56029
56316
  const checkComponent = (componentBody) => {
56030
56317
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
@@ -57629,7 +57916,6 @@ const reduxUseselectorInlineDerivation = defineRule({
57629
57916
  title: "useSelector derives data inline",
57630
57917
  severity: "warn",
57631
57918
  category: "Performance",
57632
- disabledWhen: ["react-compiler"],
57633
57919
  recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
57634
57920
  create: (context) => {
57635
57921
  let aliases = /* @__PURE__ */ new Set();
@@ -57689,7 +57975,6 @@ const reduxUseselectorReturnsNewCollection = defineRule({
57689
57975
  title: "useSelector returns a new collection",
57690
57976
  severity: "warn",
57691
57977
  category: "Performance",
57692
- disabledWhen: ["react-compiler"],
57693
57978
  recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
57694
57979
  create: (context) => {
57695
57980
  let aliases = /* @__PURE__ */ new Set();
@@ -57815,6 +58100,7 @@ const renderingHoistJsx = defineRule({
57815
58100
  title: "Constant JSX rebuilt each render",
57816
58101
  tags: ["test-noise"],
57817
58102
  severity: "warn",
58103
+ disabledWhen: ["react-compiler"],
57818
58104
  recommendation: "Move the static JSX out to the top of the file: `const ICON = <svg>...</svg>`, so it isn't rebuilt on every render",
57819
58105
  create: (context) => {
57820
58106
  let componentDepth = 0;
@@ -58261,8 +58547,9 @@ const renderingHydrationNoFlicker = defineRule({
58261
58547
  id: "rendering-hydration-no-flicker",
58262
58548
  title: "useEffect setState flashes on mount",
58263
58549
  tags: ["test-noise"],
58550
+ requires: ["ssr"],
58264
58551
  severity: "warn",
58265
- recommendation: "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
58552
+ recommendation: "Read the value with `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` inside a reusable hook, or add `suppressHydrationWarning` to the element",
58266
58553
  create: (context) => ({ CallExpression(node) {
58267
58554
  if (!isHookCall$2(node, USE_EFFECT_ONLY) || (node.arguments?.length ?? 0) < 2) return;
58268
58555
  const depsNode = node.arguments[1];
@@ -58715,7 +59002,7 @@ const renderingUsetransitionLoading = defineRule({
58715
59002
  title: "Loading useState forces extra render",
58716
59003
  tags: ["test-noise"],
58717
59004
  severity: "warn",
58718
- recommendation: "Replace with `const [isPending, startTransition] = useTransition()`, which skips the extra render for the loading flag",
59005
+ recommendation: "Replace with `const [isPending, startTransition] = useTransition()`, which marks the update as non-urgent and interruptible so the input stays responsive",
58719
59006
  create: (context) => ({ VariableDeclarator(node) {
58720
59007
  if (!isNodeOfType(node.id, "ArrayPattern") || !node.id.elements?.length) return;
58721
59008
  if (!node.init || !isHookCall$2(node.init, "useState")) return;
@@ -58747,7 +59034,7 @@ const renderingUsetransitionLoading = defineRule({
58747
59034
  }
58748
59035
  context.report({
58749
59036
  node: node.init,
58750
- message: `This adds an extra render because useState for "${stateVariableName}" re-renders just for the loading flag, so if it's a state change & not a data fetch, use useTransition instead`
59037
+ message: `This makes the "${stateVariableName}" update urgent and blocking because it's a plain useState flag, so if it's a state change & not a data fetch, use useTransition to keep the UI responsive while it runs`
58751
59038
  });
58752
59039
  } })
58753
59040
  });
@@ -59062,6 +59349,7 @@ const rerenderDependencies = defineRule({
59062
59349
  title: "Unstable value recreated every render",
59063
59350
  tags: ["test-noise"],
59064
59351
  severity: "error",
59352
+ disabledWhen: ["react-compiler"],
59065
59353
  recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
59066
59354
  create: (context) => ({ CallExpression(node) {
59067
59355
  if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
@@ -59474,7 +59762,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
59474
59762
  title: "useMemo before an early return",
59475
59763
  tags: ["test-noise"],
59476
59764
  severity: "warn",
59477
- recommendation: "Move the JSX into a child component wrapped in memo, so the parent's early return skips it",
59765
+ recommendation: "Move the JSX into a child component rendered after the early return, so renders that take the early return never build it",
59478
59766
  create: (context) => {
59479
59767
  const inspectFunctionBody = (statements) => {
59480
59768
  let memoNode = null;
@@ -59500,7 +59788,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
59500
59788
  if (callbackGuardTests.some((guardTest) => areConditionsStructurallyEqual(stmt.test, guardTest))) continue;
59501
59789
  context.report({
59502
59790
  node: memoNode,
59503
- message: "This runs even when the component bails out because the useMemo builds JSX before an early return, so move the JSX into a child wrapped in memo to skip it on the early return"
59791
+ message: "This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it"
59504
59792
  });
59505
59793
  return;
59506
59794
  }
@@ -60003,6 +60291,7 @@ const rerenderMemoWithDefaultValue = defineRule({
60003
60291
  title: "Empty default prop breaks memo",
60004
60292
  tags: ["test-noise"],
60005
60293
  severity: "warn",
60294
+ disabledWhen: ["react-compiler"],
60006
60295
  recommendation: "Move it to the top of the file: `const EMPTY_ITEMS: Item[] = []`, then use that as the default value",
60007
60296
  create: (context) => {
60008
60297
  let memoRegistry = /* @__PURE__ */ new Map();
@@ -60828,6 +61117,7 @@ const rnListDataMapped = defineRule({
60828
61117
  title: "List data rebuilt every render",
60829
61118
  tags: ["test-noise"],
60830
61119
  requires: ["react-native"],
61120
+ disabledWhen: ["react-compiler"],
60831
61121
  severity: "warn",
60832
61122
  recommendation: "This builds a new array each time the parent redraws, so every row redraws too. Wrap it in `useMemo(() => items.map(...), [items])` to keep the same array.",
60833
61123
  create: (context) => ({ JSXOpeningElement(node) {
@@ -63181,6 +63471,66 @@ const roleHasRequiredAriaProps = defineRule({
63181
63471
  } })
63182
63472
  });
63183
63473
  //#endregion
63474
+ //#region src/plugin/constants/global-aria-properties.ts
63475
+ const GLOBAL_ARIA_PROPERTIES = new Set([
63476
+ "aria-atomic",
63477
+ "aria-braillelabel",
63478
+ "aria-brailleroledescription",
63479
+ "aria-busy",
63480
+ "aria-controls",
63481
+ "aria-current",
63482
+ "aria-describedby",
63483
+ "aria-description",
63484
+ "aria-details",
63485
+ "aria-disabled",
63486
+ "aria-dropeffect",
63487
+ "aria-errormessage",
63488
+ "aria-flowto",
63489
+ "aria-grabbed",
63490
+ "aria-haspopup",
63491
+ "aria-hidden",
63492
+ "aria-invalid",
63493
+ "aria-keyshortcuts",
63494
+ "aria-label",
63495
+ "aria-labelledby",
63496
+ "aria-live",
63497
+ "aria-owns",
63498
+ "aria-relevant",
63499
+ "aria-roledescription"
63500
+ ]);
63501
+ //#endregion
63502
+ //#region src/plugin/constants/prohibited-aria-properties-by-role.ts
63503
+ const ACCESSIBLE_NAME_PROPERTIES = new Set([
63504
+ "aria-braillelabel",
63505
+ "aria-label",
63506
+ "aria-labelledby"
63507
+ ]);
63508
+ const PROHIBITED_ARIA_PROPERTIES_BY_ROLE = {
63509
+ caption: ACCESSIBLE_NAME_PROPERTIES,
63510
+ code: ACCESSIBLE_NAME_PROPERTIES,
63511
+ definition: ACCESSIBLE_NAME_PROPERTIES,
63512
+ deletion: ACCESSIBLE_NAME_PROPERTIES,
63513
+ emphasis: ACCESSIBLE_NAME_PROPERTIES,
63514
+ generic: new Set([
63515
+ "aria-braillelabel",
63516
+ "aria-brailleroledescription",
63517
+ "aria-label",
63518
+ "aria-labelledby",
63519
+ "aria-roledescription"
63520
+ ]),
63521
+ insertion: ACCESSIBLE_NAME_PROPERTIES,
63522
+ mark: ACCESSIBLE_NAME_PROPERTIES,
63523
+ none: ACCESSIBLE_NAME_PROPERTIES,
63524
+ paragraph: ACCESSIBLE_NAME_PROPERTIES,
63525
+ presentation: ACCESSIBLE_NAME_PROPERTIES,
63526
+ strong: ACCESSIBLE_NAME_PROPERTIES,
63527
+ subscript: ACCESSIBLE_NAME_PROPERTIES,
63528
+ superscript: ACCESSIBLE_NAME_PROPERTIES,
63529
+ term: ACCESSIBLE_NAME_PROPERTIES,
63530
+ time: ACCESSIBLE_NAME_PROPERTIES,
63531
+ tooltip: ACCESSIBLE_NAME_PROPERTIES
63532
+ };
63533
+ //#endregion
63184
63534
  //#region src/plugin/constants/role-supports-aria-props.ts
63185
63535
  const ROLE_SUPPORTS_ARIA_PROPS = {
63186
63536
  alert: new Set([
@@ -66281,6 +66631,12 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
66281
66631
  ])
66282
66632
  };
66283
66633
  //#endregion
66634
+ //#region src/plugin/utils/is-aria-property-supported-by-role.ts
66635
+ const isAriaPropertySupportedByRole = (role, property) => {
66636
+ if (PROHIBITED_ARIA_PROPERTIES_BY_ROLE[role]?.has(property)) return false;
66637
+ return ROLE_SUPPORTS_ARIA_PROPS[role]?.has(property) === true || GLOBAL_ARIA_PROPERTIES.has(property);
66638
+ };
66639
+ //#endregion
66284
66640
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
66285
66641
  const buildMessageDefault = (roles, propName) => {
66286
66642
  return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
@@ -66317,16 +66673,10 @@ const roleSupportsAriaProps = defineRule({
66317
66673
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
66318
66674
  const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType, context.scopes)].filter((role) => role !== null);
66319
66675
  if (roleCandidates === null || roleCandidates.length === 0) return;
66320
- const supportedSets = [];
66321
- for (const role of roleCandidates) {
66322
- if (!VALID_ARIA_ROLES.has(role)) return;
66323
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
66324
- if (!supported) return;
66325
- supportedSets.push(supported);
66326
- }
66676
+ for (const role of roleCandidates) if (!VALID_ARIA_ROLES.has(role)) return;
66327
66677
  const isImplicit = !roleAttribute;
66328
66678
  for (const { attribute, propName } of ariaAttributes) {
66329
- if (supportedSets.some((supported) => supported.has(propName))) continue;
66679
+ if (roleCandidates.some((role) => isAriaPropertySupportedByRole(role, propName))) continue;
66330
66680
  context.report({
66331
66681
  node: attribute,
66332
66682
  message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
@@ -75108,7 +75458,7 @@ const EXTERNAL_RULES = [
75108
75458
  {
75109
75459
  key: "react-hooks-js/set-state-in-effect",
75110
75460
  source: "react-compiler",
75111
- severity: "error"
75461
+ severity: "warn"
75112
75462
  },
75113
75463
  {
75114
75464
  key: "react-hooks-js/globals",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.8.1-dev.1839566",
3
+ "version": "0.8.1-dev.26b4a0c",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",