oxlint-plugin-react-doctor 0.7.6-dev.1d7b6e2 → 0.7.6-dev.3f22fe3

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +751 -434
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6004,6 +6004,170 @@ const getStaticPropertyKeyName = (node, options = {}) => {
6004
6004
  return null;
6005
6005
  };
6006
6006
  //#endregion
6007
+ //#region src/plugin/utils/are-expressions-structurally-equal.ts
6008
+ const areExpressionsStructurallyEqual = (a, b) => {
6009
+ if (!a || !b) return a === b;
6010
+ const unwrappedA = stripParenExpression(a);
6011
+ const unwrappedB = stripParenExpression(b);
6012
+ if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
6013
+ if (a.type !== b.type) return false;
6014
+ if (isNodeOfType(a, "ThisExpression")) return true;
6015
+ if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
6016
+ if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
6017
+ if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
6018
+ if (a.computed !== b.computed) return false;
6019
+ return areExpressionsStructurallyEqual(a.object, b.object) && areExpressionsStructurallyEqual(a.property, b.property);
6020
+ }
6021
+ if (isNodeOfType(a, "CallExpression") && isNodeOfType(b, "CallExpression")) {
6022
+ if (!areExpressionsStructurallyEqual(a.callee, b.callee)) return false;
6023
+ const argumentsA = a.arguments ?? [];
6024
+ const argumentsB = b.arguments ?? [];
6025
+ if (argumentsA.length !== argumentsB.length) return false;
6026
+ return argumentsA.every((argument, index) => areExpressionsStructurallyEqual(argument, argumentsB[index]));
6027
+ }
6028
+ return false;
6029
+ };
6030
+ //#endregion
6031
+ //#region src/plugin/utils/has-keyboard-activatable-descendant.ts
6032
+ const NATIVE_KEYBOARD_ACTIVATABLE_TAGS = new Set([
6033
+ "a",
6034
+ "button",
6035
+ "input",
6036
+ "select",
6037
+ "summary",
6038
+ "textarea"
6039
+ ]);
6040
+ const KEYBOARD_ACTIVATABLE_COMPONENT_NAME_PATTERN = /button|link|nav|anchor/i;
6041
+ const EQUIVALENT_ACTION_COMPONENT_NAME_PATTERN = /(?:button|link|anchor)$/i;
6042
+ const UPPERCASE_COMPONENT_NAME_PATTERN = /^[A-Z]/;
6043
+ const DESCENDANT_ACTION_PROP_NAMES = ["onClick", "onPress"];
6044
+ const isStaticallyNullish = (expression) => isNullishExpression(stripParenExpression(expression));
6045
+ const isStaticallyNullishHandlerExpression = (expression, scopes) => {
6046
+ const strippedExpression = stripParenExpression(expression);
6047
+ if (isNullishExpression(strippedExpression)) return true;
6048
+ const symbol = resolveConstIdentifierAlias(strippedExpression, scopes);
6049
+ if (symbol?.kind !== "const" || !symbol.initializer) return false;
6050
+ return isNullishExpression(stripParenExpression(symbol.initializer));
6051
+ };
6052
+ const resolveSingleHandlerAction = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
6053
+ const strippedExpression = stripParenExpression(expression);
6054
+ if (isStaticallyNullishHandlerExpression(strippedExpression, scopes)) return null;
6055
+ if (isNodeOfType(strippedExpression, "ConditionalExpression")) {
6056
+ const consequent = strippedExpression.consequent;
6057
+ const alternate = strippedExpression.alternate;
6058
+ if (isStaticallyNullishHandlerExpression(consequent, scopes)) return resolveSingleHandlerAction(alternate, scopes, visitedSymbolIds);
6059
+ if (isStaticallyNullishHandlerExpression(alternate, scopes)) return resolveSingleHandlerAction(consequent, scopes, visitedSymbolIds);
6060
+ return null;
6061
+ }
6062
+ if (isNodeOfType(strippedExpression, "Identifier")) {
6063
+ const symbol = scopes.symbolFor(strippedExpression);
6064
+ if (symbol?.kind === "const" && symbol.initializer && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") && !visitedSymbolIds.has(symbol.id)) {
6065
+ visitedSymbolIds.add(symbol.id);
6066
+ return resolveSingleHandlerAction(symbol.initializer, scopes, visitedSymbolIds);
6067
+ }
6068
+ return strippedExpression;
6069
+ }
6070
+ if (isNodeOfType(strippedExpression, "ArrowFunctionExpression") || isNodeOfType(strippedExpression, "FunctionExpression") || isNodeOfType(strippedExpression, "FunctionDeclaration")) {
6071
+ const body = stripParenExpression(strippedExpression.body);
6072
+ if (!isNodeOfType(body, "BlockStatement")) return body;
6073
+ if (body.body.length !== 1) return null;
6074
+ const statement = body.body[0];
6075
+ if (isNodeOfType(statement, "ExpressionStatement")) return statement.expression;
6076
+ if (isNodeOfType(statement, "ReturnStatement") && statement.argument) return stripParenExpression(statement.argument);
6077
+ return null;
6078
+ }
6079
+ return strippedExpression;
6080
+ };
6081
+ const getAttributeAction = (attribute, scopes) => {
6082
+ if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
6083
+ return resolveSingleHandlerAction(attribute.value.expression, scopes);
6084
+ };
6085
+ const hasPotentiallyTruthyAttribute = (openingElement, attributeName) => {
6086
+ const attribute = hasJsxPropIgnoreCase(openingElement.attributes, attributeName);
6087
+ if (!attribute) return false;
6088
+ if (!attribute.value) return true;
6089
+ if (isNodeOfType(attribute.value, "Literal")) return attribute.value.value === true || attribute.value.value === "true";
6090
+ if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return true;
6091
+ const expression = stripParenExpression(attribute.value.expression);
6092
+ return !isNodeOfType(expression, "Literal") || expression.value !== false;
6093
+ };
6094
+ const hasPotentiallyNonEmptyAttribute = (openingElement, attributeName) => {
6095
+ const attribute = hasJsxPropIgnoreCase(openingElement.attributes, attributeName);
6096
+ if (!attribute?.value) return false;
6097
+ if (isNodeOfType(attribute.value, "Literal")) return typeof attribute.value.value === "string" && attribute.value.value.trim().length > 0;
6098
+ if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return true;
6099
+ const expression = stripParenExpression(attribute.value.expression);
6100
+ if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" && expression.value.trim().length > 0;
6101
+ return !isStaticallyNullish(expression);
6102
+ };
6103
+ const childMayProvideAccessibleName = (child) => {
6104
+ if (isNodeOfType(child, "JSXText")) return child.value.trim().length > 0;
6105
+ if (isNodeOfType(child, "JSXElement")) return hasAccessibleNameEvidence(child);
6106
+ if (isNodeOfType(child, "JSXFragment")) return child.children.some((nestedChild) => childMayProvideAccessibleName(nestedChild));
6107
+ if (!isNodeOfType(child, "JSXExpressionContainer")) return false;
6108
+ const expression = stripParenExpression(child.expression);
6109
+ if (isNodeOfType(expression, "Literal")) {
6110
+ if (typeof expression.value === "string") return expression.value.trim().length > 0;
6111
+ return typeof expression.value === "number";
6112
+ }
6113
+ if (isStaticallyNullish(expression)) return false;
6114
+ if (isNodeOfType(expression, "JSXElement")) return hasAccessibleNameEvidence(expression);
6115
+ if (isNodeOfType(expression, "JSXFragment")) return expression.children.some((nestedChild) => childMayProvideAccessibleName(nestedChild));
6116
+ return true;
6117
+ };
6118
+ const hasAccessibleNameEvidence = (element) => {
6119
+ if (hasPotentiallyNonEmptyAttribute(element.openingElement, "aria-label") || hasPotentiallyNonEmptyAttribute(element.openingElement, "aria-labelledby")) return true;
6120
+ return element.children.some((child) => childMayProvideAccessibleName(child));
6121
+ };
6122
+ const hasNegativeStaticTabIndex = (openingElement) => {
6123
+ const tabIndexAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "tabIndex");
6124
+ if (!tabIndexAttribute?.value) return false;
6125
+ const tabIndexValue = parseJsxValue(tabIndexAttribute.value);
6126
+ return tabIndexValue !== null && tabIndexValue < 0;
6127
+ };
6128
+ const isHiddenSubtreeRoot = (openingElement, settings) => isHiddenFromScreenReader(openingElement, settings) || hasPotentiallyTruthyAttribute(openingElement, "hidden");
6129
+ const isKeyboardActivatableElement = (element, requiresAccessibleName) => {
6130
+ const openingElement = element.openingElement;
6131
+ const elementName = flattenJsxName$1(openingElement.name);
6132
+ if (!elementName) return false;
6133
+ if (HTML_TAGS.has(elementName)) {
6134
+ if (!NATIVE_KEYBOARD_ACTIVATABLE_TAGS.has(elementName)) return false;
6135
+ } else if (requiresAccessibleName) {
6136
+ if (!UPPERCASE_COMPONENT_NAME_PATTERN.test(elementName) || !EQUIVALENT_ACTION_COMPONENT_NAME_PATTERN.test(elementName)) return false;
6137
+ } else if (!UPPERCASE_COMPONENT_NAME_PATTERN.test(elementName) || !KEYBOARD_ACTIVATABLE_COMPONENT_NAME_PATTERN.test(elementName)) return false;
6138
+ if (!requiresAccessibleName) return true;
6139
+ if (elementName === "a" && !hasJsxPropIgnoreCase(openingElement.attributes, "href")) return false;
6140
+ if (hasPotentiallyTruthyAttribute(openingElement, "disabled") || hasPotentiallyTruthyAttribute(openingElement, "isDisabled") || hasPotentiallyTruthyAttribute(openingElement, "aria-disabled") || hasNegativeStaticTabIndex(openingElement)) return false;
6141
+ return hasAccessibleNameEvidence(element);
6142
+ };
6143
+ const findKeyboardActivatableDescendant = (node, expectedAction, scopes, settings) => {
6144
+ const walk = (descendant) => findKeyboardActivatableDescendant(descendant, expectedAction, scopes, settings);
6145
+ if (isNodeOfType(node, "JSXElement")) {
6146
+ if (expectedAction && isHiddenSubtreeRoot(node.openingElement, settings)) return false;
6147
+ if (isKeyboardActivatableElement(node, expectedAction !== null)) {
6148
+ if (!expectedAction) return true;
6149
+ for (const actionPropName of DESCENDANT_ACTION_PROP_NAMES) {
6150
+ const attribute = hasJsxPropIgnoreCase(node.openingElement.attributes, actionPropName);
6151
+ const action = attribute ? getAttributeAction(attribute, scopes) : null;
6152
+ if (action && areExpressionsStructurallyEqual(expectedAction, action)) return true;
6153
+ }
6154
+ }
6155
+ return node.children.some((child) => walk(child));
6156
+ }
6157
+ if (isNodeOfType(node, "JSXFragment")) return node.children.some((child) => walk(child));
6158
+ if (!expectedAction) return false;
6159
+ if (isNodeOfType(node, "JSXExpressionContainer")) return walk(node.expression);
6160
+ if (isNodeOfType(node, "LogicalExpression")) return walk(node.left) || walk(node.right);
6161
+ if (isNodeOfType(node, "ConditionalExpression")) return walk(node.consequent) || walk(node.alternate);
6162
+ return false;
6163
+ };
6164
+ const hasKeyboardActivatableDescendant = (element, interactionAttribute, scopes, settings) => {
6165
+ if (!element || !isNodeOfType(element, "JSXElement")) return false;
6166
+ const expectedAction = interactionAttribute ? getAttributeAction(interactionAttribute, scopes) : null;
6167
+ if (interactionAttribute && !expectedAction) return false;
6168
+ return element.children.some((child) => findKeyboardActivatableDescendant(child, expectedAction, scopes, settings));
6169
+ };
6170
+ //#endregion
6007
6171
  //#region src/plugin/utils/is-presentation-role.ts
6008
6172
  const isPresentationRole = (openingElement) => {
6009
6173
  const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
@@ -6012,6 +6176,53 @@ const isPresentationRole = (openingElement) => {
6012
6176
  return value !== null && PRESENTATION_ROLES$1.has(value);
6013
6177
  };
6014
6178
  //#endregion
6179
+ //#region src/plugin/utils/get-property-key-name.ts
6180
+ const getPropertyKeyName$2 = (propertyKey) => {
6181
+ if (isNodeOfType(propertyKey, "Identifier") || isNodeOfType(propertyKey, "PrivateIdentifier")) return propertyKey.name;
6182
+ };
6183
+ //#endregion
6184
+ //#region src/plugin/utils/resolve-member-handler-function.ts
6185
+ const asHandlerFunction = (value) => {
6186
+ if (isNodeOfType(value, "FunctionExpression") || isNodeOfType(value, "ArrowFunctionExpression")) return value;
6187
+ };
6188
+ const resolveFromClassBody = (classBody, propertyName) => {
6189
+ for (const classElement of classBody.body) {
6190
+ if (!isNodeOfType(classElement, "MethodDefinition") && !isNodeOfType(classElement, "PropertyDefinition")) continue;
6191
+ if (getPropertyKeyName$2(classElement.key) !== propertyName) continue;
6192
+ const resolvedHandler = asHandlerFunction(classElement.value);
6193
+ if (resolvedHandler) return resolvedHandler;
6194
+ }
6195
+ };
6196
+ const resolveFromObjectExpression = (objectExpression, propertyName) => {
6197
+ for (const objectProperty of objectExpression.properties) {
6198
+ if (!isNodeOfType(objectProperty, "Property")) continue;
6199
+ if (getPropertyKeyName$2(objectProperty.key) !== propertyName) continue;
6200
+ const resolvedHandler = asHandlerFunction(objectProperty.value);
6201
+ if (resolvedHandler) return resolvedHandler;
6202
+ }
6203
+ };
6204
+ const resolveMemberHandlerFunction = (handler) => {
6205
+ const propertyName = getPropertyKeyName$2(handler.property);
6206
+ if (propertyName === void 0) return void 0;
6207
+ const objectNode = handler.object;
6208
+ if (isNodeOfType(objectNode, "ThisExpression")) {
6209
+ let ancestor = handler.parent;
6210
+ while (ancestor) {
6211
+ if (isNodeOfType(ancestor, "ClassBody")) return resolveFromClassBody(ancestor, propertyName);
6212
+ if (isNodeOfType(ancestor, "ObjectExpression")) {
6213
+ const resolvedHandler = resolveFromObjectExpression(ancestor, propertyName);
6214
+ if (resolvedHandler) return resolvedHandler;
6215
+ }
6216
+ ancestor = ancestor.parent;
6217
+ }
6218
+ return;
6219
+ }
6220
+ if (isNodeOfType(objectNode, "Identifier")) {
6221
+ const binding = findVariableInitializer(objectNode, objectNode.name);
6222
+ if (isNodeOfType(binding?.initializer, "ObjectExpression")) return resolveFromObjectExpression(binding.initializer, propertyName);
6223
+ }
6224
+ };
6225
+ //#endregion
6015
6226
  //#region src/plugin/utils/is-pure-event-blocker-handler.ts
6016
6227
  const BLOCKER_METHOD_NAMES = new Set([
6017
6228
  "stopPropagation",
@@ -6043,9 +6254,10 @@ const isPureEventBlockerBody = (body) => {
6043
6254
  };
6044
6255
  const isPureEventBlockerHandler = (attribute) => {
6045
6256
  if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return false;
6046
- const expression = attribute.value.expression;
6257
+ const expression = stripParenExpression(attribute.value.expression);
6047
6258
  if (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression")) return isPureEventBlockerBody(expression.body);
6048
- return false;
6259
+ if (!isNodeOfType(expression, "MemberExpression")) return false;
6260
+ return isPureEventBlockerBody(resolveMemberHandlerFunction(expression)?.body);
6049
6261
  };
6050
6262
  //#endregion
6051
6263
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
@@ -6197,29 +6409,6 @@ const hasCompositeItemRole = (node) => {
6197
6409
  const firstRole = roleValue.value.split(/\s+/)[0];
6198
6410
  return Boolean(firstRole && COMPOSITE_ITEM_ROLES$1.has(firstRole.toLowerCase()));
6199
6411
  };
6200
- const NATIVE_ACTIVATABLE_TAGS = new Set([
6201
- "a",
6202
- "button",
6203
- "input",
6204
- "select",
6205
- "textarea",
6206
- "summary"
6207
- ]);
6208
- const INTERACTIVE_COMPONENT_NAME_PATTERN = /button|link|nav|anchor/i;
6209
- const containsKeyboardActivatableDescendant = (element) => {
6210
- if (!element || !isNodeOfType(element, "JSXElement")) return false;
6211
- for (const child of element.children) {
6212
- const childNode = child;
6213
- if (!isNodeOfType(childNode, "JSXElement")) continue;
6214
- const name = flattenJsxName$1(childNode.openingElement.name);
6215
- if (name) {
6216
- if (NATIVE_ACTIVATABLE_TAGS.has(name)) return true;
6217
- if (/^[A-Z]/.test(name) && INTERACTIVE_COMPONENT_NAME_PATTERN.test(name)) return true;
6218
- }
6219
- if (containsKeyboardActivatableDescendant(childNode)) return true;
6220
- }
6221
- return false;
6222
- };
6223
6412
  const isTargetCurrentTargetComparison = (node) => {
6224
6413
  if (!isNodeOfType(node, "BinaryExpression")) return false;
6225
6414
  if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==") return false;
@@ -6276,7 +6465,8 @@ const clickEventsHaveKeyEvents = defineRule({
6276
6465
  if (hasCompositeItemRole(node)) return;
6277
6466
  if (isHoverSelectionListItem(tag, node)) return;
6278
6467
  if (onClick && isBackdropDismissHandler(onClick)) return;
6279
- if (containsKeyboardActivatableDescendant(node.parent)) return;
6468
+ if (hasKeyboardActivatableDescendant(node.parent, null, context.scopes, context.settings)) return;
6469
+ if (onClick && hasKeyboardActivatableDescendant(node.parent, onClick, context.scopes, context.settings)) return;
6280
6470
  if (isHiddenFromScreenReader(node, context.settings)) return;
6281
6471
  if (isPresentationRole(node)) return;
6282
6472
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
@@ -8159,50 +8349,6 @@ const assignedHandlerCallsPreventDefault = (scopeOwner, handlerName) => {
8159
8349
  });
8160
8350
  return didFindPreventDefault;
8161
8351
  };
8162
- const asHandlerFunction = (value) => {
8163
- if (!value) return void 0;
8164
- if (isNodeOfType(value, "FunctionExpression") || isNodeOfType(value, "ArrowFunctionExpression")) return value;
8165
- };
8166
- const memberKeyName = (keyNode) => {
8167
- if (isNodeOfType(keyNode, "Identifier") || isNodeOfType(keyNode, "PrivateIdentifier")) return keyNode.name;
8168
- };
8169
- const resolveFromClassBody = (classBody, propertyName) => {
8170
- for (const element of classBody.body ?? []) {
8171
- if (!isNodeOfType(element, "MethodDefinition") && !isNodeOfType(element, "PropertyDefinition")) continue;
8172
- if (memberKeyName(element.key) !== propertyName) continue;
8173
- const resolved = asHandlerFunction(element.value);
8174
- if (resolved) return resolved;
8175
- }
8176
- };
8177
- const resolveFromObjectExpression = (objectExpression, propertyName) => {
8178
- for (const objectProperty of objectExpression.properties ?? []) {
8179
- if (!isNodeOfType(objectProperty, "Property")) continue;
8180
- if (memberKeyName(objectProperty.key) !== propertyName) continue;
8181
- const resolved = asHandlerFunction(objectProperty.value);
8182
- if (resolved) return resolved;
8183
- }
8184
- };
8185
- const resolveMemberHandlerFunction = (handler) => {
8186
- const propertyName = memberKeyName(handler.property);
8187
- if (propertyName === void 0) return void 0;
8188
- const objectNode = handler.object;
8189
- if (isNodeOfType(objectNode, "ThisExpression")) {
8190
- let ancestor = handler.parent;
8191
- while (ancestor) {
8192
- if (isNodeOfType(ancestor, "ClassBody")) return resolveFromClassBody(ancestor, propertyName);
8193
- if (isNodeOfType(ancestor, "ObjectExpression")) {
8194
- const resolved = resolveFromObjectExpression(ancestor, propertyName);
8195
- if (resolved) return resolved;
8196
- }
8197
- ancestor = ancestor.parent ?? null;
8198
- }
8199
- return;
8200
- }
8201
- if (isNodeOfType(objectNode, "Identifier")) {
8202
- const initializer = findVariableInitializer(objectNode, objectNode.name)?.initializer;
8203
- if (initializer && isNodeOfType(initializer, "ObjectExpression")) return resolveFromObjectExpression(initializer, propertyName);
8204
- }
8205
- };
8206
8352
  const handlerArgumentCallsPreventDefault = (handler) => {
8207
8353
  if (!handler) return false;
8208
8354
  if (handlerCallsPreventDefault(handler)) return true;
@@ -8364,7 +8510,7 @@ const handlerMayExposeEvent = (handler, context) => {
8364
8510
  }
8365
8511
  const propertyName = getStaticPropertyName(expression);
8366
8512
  if (isNodeOfType(memberObject, "ObjectExpression") && propertyName) {
8367
- const property = memberObject.properties.find((candidate) => isNodeOfType(candidate, "Property") && memberKeyName(candidate.key) === propertyName);
8513
+ const property = memberObject.properties.find((candidate) => isNodeOfType(candidate, "Property") && getPropertyKeyName$2(candidate.key) === propertyName);
8368
8514
  return Boolean(isNodeOfType(property, "Property") && expressionContainsParameterAlias(property.value));
8369
8515
  }
8370
8516
  if (isNodeOfType(memberObject, "ArrayExpression") && isNodeOfType(expression.property, "Literal") && typeof expression.property.value === "number") {
@@ -8405,9 +8551,9 @@ const handlerMayExposeEvent = (handler, context) => {
8405
8551
  continue;
8406
8552
  }
8407
8553
  if (!isNodeOfType(patternProperty, "Property")) continue;
8408
- const propertyName = memberKeyName(patternProperty.key);
8554
+ const propertyName = getPropertyKeyName$2(patternProperty.key);
8409
8555
  if (!propertyName) continue;
8410
- const sourceProperty = source.properties.find((property) => isNodeOfType(property, "Property") && memberKeyName(property.key) === propertyName);
8556
+ const sourceProperty = source.properties.find((property) => isNodeOfType(property, "Property") && getPropertyKeyName$2(property.key) === propertyName);
8411
8557
  if (isNodeOfType(sourceProperty, "Property")) addAliasesFromPattern(patternProperty.value, sourceProperty.value);
8412
8558
  }
8413
8559
  return;
@@ -10279,21 +10425,29 @@ const isListenerPathAmbiguous = (node, bodyNode, allowAmbiguousChild) => {
10279
10425
  };
10280
10426
  //#endregion
10281
10427
  //#region src/plugin/rules/state-and-effects/utils/resolve-event-listener-capture.ts
10282
- const resolveEventListenerCapture = (optionsNode) => {
10428
+ const resolveEventListenerCapture = (optionsNode, { allowComputedString = false, allowIndeterminateEntries = false } = {}) => {
10283
10429
  if (!optionsNode) return false;
10284
10430
  const unwrappedOptions = stripParenExpression(optionsNode);
10285
10431
  if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? unwrappedOptions.value : null;
10286
10432
  if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
10287
10433
  let capture = false;
10288
- for (const property of unwrappedOptions.properties) {
10434
+ for (const property of unwrappedOptions.properties ?? []) {
10435
+ if (allowIndeterminateEntries && isNodeOfType(property, "SpreadElement")) {
10436
+ capture = null;
10437
+ continue;
10438
+ }
10289
10439
  if (!isNodeOfType(property, "Property")) return null;
10290
- const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
10440
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString });
10291
10441
  if (propertyName === null) return null;
10292
10442
  if (!property.computed && propertyName === "__proto__") return null;
10293
10443
  if (propertyName !== "capture") continue;
10294
10444
  const propertyValue = stripParenExpression(property.value);
10295
- if (!isNodeOfType(propertyValue, "Literal") || typeof propertyValue.value !== "boolean") return null;
10296
- capture = propertyValue.value;
10445
+ if (isNodeOfType(propertyValue, "Literal") && typeof propertyValue.value === "boolean") {
10446
+ capture = propertyValue.value;
10447
+ continue;
10448
+ }
10449
+ if (!allowIndeterminateEntries) return null;
10450
+ capture = null;
10297
10451
  }
10298
10452
  return capture;
10299
10453
  };
@@ -10536,7 +10690,7 @@ const readListenerCandidate = (node, methodName, context) => {
10536
10690
  const targetKey = resolveTargetKey(targetNode, context);
10537
10691
  const eventName = resolveStaticEventName(node.arguments?.[0], context);
10538
10692
  const callbackIdentity = resolveCallbackIdentity(node.arguments?.[1], context);
10539
- const capture = resolveEventListenerCapture(node.arguments?.[2]);
10693
+ const capture = resolveEventListenerCapture(node.arguments?.[2], { allowComputedString: true });
10540
10694
  if (targetKey === null || eventName === null) return null;
10541
10695
  return {
10542
10696
  node,
@@ -10570,7 +10724,7 @@ const readDestructuredRemovalCandidate = (node, context) => {
10570
10724
  targetKey,
10571
10725
  eventName,
10572
10726
  callbackIdentity: resolveCallbackIdentity(node.arguments?.[2], context),
10573
- capture: resolveEventListenerCapture(node.arguments?.[3])
10727
+ capture: resolveEventListenerCapture(node.arguments?.[3], { allowComputedString: true })
10574
10728
  };
10575
10729
  };
10576
10730
  const resolveReturnedCleanupBody = (returnedValue, context) => {
@@ -10948,7 +11102,7 @@ const findDeclaratorForBinding = (bindingIdentifier) => {
10948
11102
  };
10949
11103
  //#endregion
10950
11104
  //#region src/plugin/utils/executes-during-render.ts
10951
- const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
11105
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
10952
11106
  "map",
10953
11107
  "filter",
10954
11108
  "forEach",
@@ -10984,7 +11138,7 @@ const executesDuringRender = (functionNode, scopes) => {
10984
11138
  if (parent.callee === functionNode) return true;
10985
11139
  if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
10986
11140
  if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
10987
- return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
11141
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
10988
11142
  };
10989
11143
  //#endregion
10990
11144
  //#region src/plugin/utils/find-render-phase-component-or-hook.ts
@@ -11015,6 +11169,26 @@ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(
11015
11169
  //#region src/plugin/utils/is-event-handler-attribute.ts
11016
11170
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
11017
11171
  //#endregion
11172
+ //#region src/plugin/utils/react-ref-origin.ts
11173
+ const resolveReactRefSymbol = (memberExpression, scopes) => {
11174
+ const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
11175
+ if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
11176
+ const symbol = resolveConstIdentifierAlias(receiver, scopes);
11177
+ if (!symbol?.initializer) return null;
11178
+ const initializer = stripParenExpression(symbol.initializer);
11179
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
11180
+ return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
11181
+ };
11182
+ const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
11183
+ const expression = stripParenExpression(node);
11184
+ if (resolveReactRefSymbol(expression, scopes)) return true;
11185
+ if (!isNodeOfType(expression, "Identifier")) return false;
11186
+ const symbol = resolveConstIdentifierAlias(expression, scopes);
11187
+ if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
11188
+ visitedSymbolIds.add(symbol.id);
11189
+ return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
11190
+ };
11191
+ //#endregion
11018
11192
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
11019
11193
  const walkInsideStatementBlocks = (node, visitor) => {
11020
11194
  if (!node || typeof node !== "object") return;
@@ -11051,6 +11225,7 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
11051
11225
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
11052
11226
  const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
11053
11227
  const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
11228
+ const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
11054
11229
  const RESOURCE_NOUN_BY_KIND = {
11055
11230
  subscribe: "subscription",
11056
11231
  timer: "timer",
@@ -11280,9 +11455,108 @@ const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
11280
11455
  return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
11281
11456
  });
11282
11457
  };
11458
+ const findForOfStatementForIteratorExpression = (expression, context) => {
11459
+ if (!expression) return null;
11460
+ const unwrappedExpression = stripParenExpression(expression);
11461
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
11462
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
11463
+ const bindingDeclarator = symbol?.bindingIdentifier.parent;
11464
+ const bindingDeclaration = bindingDeclarator?.parent;
11465
+ const forOfStatement = bindingDeclaration?.parent;
11466
+ const isStableIteratorBinding = isNodeOfType(bindingDeclaration, "VariableDeclaration") && symbol?.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier));
11467
+ return symbol && isNodeOfType(bindingDeclarator, "VariableDeclarator") && bindingDeclarator.id === symbol.bindingIdentifier && isStableIteratorBinding && bindingDeclaration.declarations.length === 1 && isNodeOfType(forOfStatement, "ForOfStatement") && forOfStatement.left === bindingDeclaration && forOfStatement.await !== true ? forOfStatement : null;
11468
+ };
11469
+ const isAssignmentFormForOfIteratorReference = (expression, context) => {
11470
+ if (!expression) return false;
11471
+ const unwrappedExpression = stripParenExpression(expression);
11472
+ const referencedSymbolIds = /* @__PURE__ */ new Set();
11473
+ walkAst(unwrappedExpression, (expressionChild) => {
11474
+ if (!isNodeOfType(expressionChild, "Identifier")) return;
11475
+ const symbol = context.scopes.symbolFor(expressionChild);
11476
+ if (symbol) referencedSymbolIds.add(symbol.id);
11477
+ });
11478
+ if (referencedSymbolIds.size === 0) return false;
11479
+ const assignsReferencedSymbol = (root, requireAssignmentTarget) => {
11480
+ let didAssignReferencedSymbol = false;
11481
+ walkAst(root, (child) => {
11482
+ if (!isNodeOfType(child, "Identifier")) return;
11483
+ if (requireAssignmentTarget && !isWithinAssignmentTarget(child)) return;
11484
+ const childSymbol = context.scopes.symbolFor(child);
11485
+ if (childSymbol && referencedSymbolIds.has(childSymbol.id)) {
11486
+ didAssignReferencedSymbol = true;
11487
+ return false;
11488
+ }
11489
+ });
11490
+ return didAssignReferencedSymbol;
11491
+ };
11492
+ let currentNode = unwrappedExpression.parent;
11493
+ while (currentNode && !isFunctionLike$2(currentNode)) {
11494
+ if (isNodeOfType(currentNode, "ForOfStatement")) {
11495
+ const loopTarget = stripParenExpression(currentNode.left);
11496
+ if (!isNodeOfType(loopTarget, "VariableDeclaration") && (assignsReferencedSymbol(loopTarget, false) || assignsReferencedSymbol(currentNode.body, true))) return true;
11497
+ }
11498
+ currentNode = currentNode.parent;
11499
+ }
11500
+ return false;
11501
+ };
11502
+ const isPrivatePlainConstIdentifier = (identifier, context) => {
11503
+ if (!isNodeOfType(identifier, "Identifier")) return false;
11504
+ const symbol = context.scopes.symbolFor(identifier);
11505
+ if (!symbol || symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
11506
+ return !isNodeOfType(symbol.declarationNode.parent?.parent, "ExportNamedDeclaration") && !isNodeOfType(symbol.declarationNode.parent?.parent, "ExportDefaultDeclaration");
11507
+ };
11508
+ const hasOnlyReplayableCollectionReferences = (identifier, context, visitedSymbolIds) => {
11509
+ if (!isNodeOfType(identifier, "Identifier") || !isPrivatePlainConstIdentifier(identifier, context)) return false;
11510
+ const symbol = context.scopes.symbolFor(identifier);
11511
+ if (!symbol) return false;
11512
+ if (visitedSymbolIds.has(symbol.id)) return true;
11513
+ visitedSymbolIds.add(symbol.id);
11514
+ return symbol.references.every((reference) => {
11515
+ const referenceRoot = findTransparentExpressionRoot(reference.identifier);
11516
+ const parent = referenceRoot.parent;
11517
+ if (isNodeOfType(parent, "ForOfStatement") && parent.right === referenceRoot) return true;
11518
+ if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot) {
11519
+ const declaration = parent.parent;
11520
+ return isNodeOfType(parent.id, "Identifier") && isNodeOfType(declaration, "VariableDeclaration") && declaration.kind === "const" ? hasOnlyReplayableCollectionReferences(parent.id, context, visitedSymbolIds) : false;
11521
+ }
11522
+ return false;
11523
+ });
11524
+ };
11525
+ const resolveReplayableIteratorCollectionKeyUncached = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
11526
+ if (!expression) return null;
11527
+ const unwrappedExpression = stripParenExpression(expression);
11528
+ if (!isNodeOfType(unwrappedExpression, "Identifier") || !isPrivatePlainConstIdentifier(unwrappedExpression, context)) return null;
11529
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
11530
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
11531
+ visitedSymbolIds.add(symbol.id);
11532
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
11533
+ if (isNodeOfType(initializer, "ArrayExpression")) return (initializer.elements ?? []).every((element) => element === null || isNodeOfType(element, "Literal") && (element.value === null || typeof element.value === "boolean" || typeof element.value === "number" || typeof element.value === "string")) && hasOnlyReplayableCollectionReferences(symbol.bindingIdentifier, context, /* @__PURE__ */ new Set()) ? `symbol:${symbol.id}` : null;
11534
+ if (!isNodeOfType(initializer, "Identifier")) return null;
11535
+ return resolveReplayableIteratorCollectionKeyUncached(initializer, context, visitedSymbolIds);
11536
+ };
11537
+ const resolveReplayableIteratorCollectionKey = (expression, context) => {
11538
+ if (!expression) return null;
11539
+ const unwrappedExpression = stripParenExpression(expression);
11540
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
11541
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
11542
+ if (!symbol) return null;
11543
+ let contextCache = REPLAYABLE_ITERATOR_COLLECTION_CACHE.get(context);
11544
+ if (!contextCache) {
11545
+ contextCache = /* @__PURE__ */ new Map();
11546
+ REPLAYABLE_ITERATOR_COLLECTION_CACHE.set(context, contextCache);
11547
+ }
11548
+ if (contextCache.has(symbol.id)) return contextCache.get(symbol.id) ?? null;
11549
+ const collectionKey = resolveReplayableIteratorCollectionKeyUncached(expression, context);
11550
+ contextCache.set(symbol.id, collectionKey);
11551
+ return collectionKey;
11552
+ };
11283
11553
  const resolveIteratorCollectionKey = (expression, context) => {
11284
- if (!expression || !isNodeOfType(expression, "Identifier")) return null;
11285
- const symbol = context.scopes.symbolFor(expression);
11554
+ if (!expression) return null;
11555
+ const unwrappedExpression = stripParenExpression(expression);
11556
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
11557
+ const forOfStatement = findForOfStatementForIteratorExpression(unwrappedExpression, context);
11558
+ if (forOfStatement) return resolveReplayableIteratorCollectionKey(forOfStatement.right, context);
11559
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
11286
11560
  if (!symbol || symbol.kind !== "parameter") return null;
11287
11561
  let callbackNode = symbol.bindingIdentifier.parent;
11288
11562
  while (callbackNode && !isFunctionLike$2(callbackNode)) callbackNode = callbackNode.parent;
@@ -11296,6 +11570,34 @@ const resolveIteratorCollectionKey = (expression, context) => {
11296
11570
  }
11297
11571
  return null;
11298
11572
  };
11573
+ const isStableLoopReceiver = (expression, context) => {
11574
+ if (!expression) return false;
11575
+ const unwrappedExpression = stripParenExpression(expression);
11576
+ return isNodeOfType(unwrappedExpression, "Identifier") && unwrappedExpression.name === "document" && context.scopes.isGlobalReference(unwrappedExpression);
11577
+ };
11578
+ const resolveStableLoopHandlerSymbolId = (expression, context) => {
11579
+ if (!expression) return null;
11580
+ const unwrappedExpression = stripParenExpression(expression);
11581
+ if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
11582
+ const symbol = context.scopes.symbolFor(unwrappedExpression);
11583
+ if (!symbol || symbol.kind !== "const" && symbol.kind !== "function" && symbol.kind !== "parameter" || !symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) return null;
11584
+ return symbol.id;
11585
+ };
11586
+ const isDirectExhaustiveForOfRelease = (releaseNode, forOfStatement) => {
11587
+ const releaseStatement = findTransparentExpressionRoot(releaseNode).parent;
11588
+ if (!isNodeOfType(releaseStatement, "ExpressionStatement")) return false;
11589
+ if (!(isNodeOfType(forOfStatement.body, "BlockStatement") ? releaseStatement.parent === forOfStatement.body : releaseStatement === forOfStatement.body)) return false;
11590
+ let hasAbruptLoopExit = false;
11591
+ walkAst(forOfStatement.body, (child) => {
11592
+ if (hasAbruptLoopExit) return false;
11593
+ if (child !== forOfStatement.body && isFunctionLike$2(child)) return false;
11594
+ if (isNodeOfType(child, "BreakStatement") || isNodeOfType(child, "ContinueStatement") || isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement")) {
11595
+ hasAbruptLoopExit = true;
11596
+ return false;
11597
+ }
11598
+ });
11599
+ return !hasAbruptLoopExit;
11600
+ };
11299
11601
  const findCollectionMappingCall = (callbackNode) => {
11300
11602
  if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
11301
11603
  const callNode = callbackNode.parent;
@@ -11443,23 +11745,26 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
11443
11745
  if (!isFunctionLike$2(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
11444
11746
  visitedFunctions.add(cleanupFunction);
11445
11747
  let didCleanupFunctionMatch = false;
11748
+ const matchingLoopOrHelperAnchors = [];
11446
11749
  walkAst(cleanupFunction.body, (cleanupChild) => {
11447
11750
  if (didCleanupFunctionMatch) return false;
11448
11751
  if (cleanupChild !== cleanupFunction.body && isFunctionLike$2(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
11752
+ const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
11449
11753
  if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
11450
- didCleanupFunctionMatch = true;
11451
- return false;
11754
+ const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context);
11755
+ if (!cleanupForOfStatement) {
11756
+ didCleanupFunctionMatch = true;
11757
+ return false;
11758
+ }
11759
+ if (!cleanupFunction.async && !cleanupFunction.generator && isDirectExhaustiveForOfRelease(cleanupChild, cleanupForOfStatement)) matchingLoopOrHelperAnchors.push(cleanupForOfStatement);
11760
+ return;
11452
11761
  }
11453
- const helperCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
11454
- if (!isNodeOfType(helperCall, "CallExpression")) return;
11455
- const stableHelperFunction = resolveStableValue(helperCall.callee, context);
11762
+ if (!isNodeOfType(cleanupCall, "CallExpression")) return;
11763
+ const stableHelperFunction = resolveStableValue(cleanupCall.callee, context);
11456
11764
  const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
11457
- if (helperFunction && isFunctionLike$2(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions)) {
11458
- didCleanupFunctionMatch = true;
11459
- return false;
11460
- }
11765
+ if (helperFunction && isFunctionLike$2(helperFunction) && !helperFunction.async && !helperFunction.generator && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, new Set(visitedFunctions))) matchingLoopOrHelperAnchors.push(cleanupCall);
11461
11766
  });
11462
- return didCleanupFunctionMatch;
11767
+ return didCleanupFunctionMatch || doMatchingNodesCoverEveryPathFromFunctionEntry(cleanupFunction, matchingLoopOrHelperAnchors, context);
11463
11768
  };
11464
11769
  const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
11465
11770
  const callExpression = stripParenExpression(expression);
@@ -11476,6 +11781,7 @@ const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
11476
11781
  };
11477
11782
  const callbackReturnsCleanupForUsage = (callback, usage, context) => {
11478
11783
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
11784
+ if (callback.async) return false;
11479
11785
  const doesReturnedValueReleaseUsage = (returnedValue) => {
11480
11786
  if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
11481
11787
  const cleanupFunction = resolveStableValue(returnedValue, context);
@@ -11540,6 +11846,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
11540
11846
  const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
11541
11847
  const effectHasCleanupForUsage = (callback, usage, context) => {
11542
11848
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
11849
+ if (callback.async) return false;
11543
11850
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
11544
11851
  if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
11545
11852
  const matchingCleanupReturns = [];
@@ -11663,10 +11970,29 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
11663
11970
  const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
11664
11971
  if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
11665
11972
  const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
11973
+ const usageEventArgument = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[0] : null;
11974
+ const releaseEventArgument = callNode.arguments?.[0];
11975
+ if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
11666
11976
  if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
11667
- const usageIteratorCollectionKey = isNodeOfType(usage.node, "CallExpression") ? resolveIteratorCollectionKey(usage.node.arguments?.[0], context) : null;
11977
+ if (!isNodeOfType(usage.node, "CallExpression")) return false;
11978
+ const usageForOfStatement = findForOfStatementForIteratorExpression(usageEventArgument, context);
11979
+ const releaseForOfStatement = findForOfStatementForIteratorExpression(releaseEventArgument, context);
11980
+ if (usageForOfStatement === null !== (releaseForOfStatement === null)) return false;
11981
+ const usageIteratorCollectionKey = resolveIteratorCollectionKey(usage.node.arguments?.[0], context);
11668
11982
  const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
11669
11983
  if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
11984
+ if (usageForOfStatement && releaseForOfStatement) {
11985
+ const registrationHandlerSymbolId = resolveStableLoopHandlerSymbolId(usage.node.arguments?.[1], context);
11986
+ const releaseHandlerSymbolId = resolveStableLoopHandlerSymbolId(callNode.arguments?.[1], context);
11987
+ if (usage.registrationVerbName !== "addEventListener" || releaseVerbName !== "removeEventListener") return false;
11988
+ const registrationCallee = stripParenExpression(usage.node.callee);
11989
+ if (!isNodeOfType(registrationCallee, "MemberExpression")) return false;
11990
+ if (!isStableLoopReceiver(registrationCallee.object, context) || !isStableLoopReceiver(callee.object, context) || registrationHandlerSymbolId === null || registrationHandlerSymbolId !== releaseHandlerSymbolId) return false;
11991
+ const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
11992
+ const releaseCapture = resolveEventListenerCapture(callNode.arguments?.[2], { allowIndeterminateEntries: true });
11993
+ if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
11994
+ if (!isDirectExhaustiveForOfRelease(callNode, releaseForOfStatement)) return false;
11995
+ }
11670
11996
  }
11671
11997
  if (releaseVerbName === "on") {
11672
11998
  const handlerArgument = callNode.arguments?.[1];
@@ -11721,6 +12047,213 @@ const fileContainsReleaseForUsage = (usage, context) => {
11721
12047
  });
11722
12048
  return didFindRelease;
11723
12049
  };
12050
+ const resolveRefOwnedCleanupFunction = (expression, context) => {
12051
+ const resolvedExpression = resolveStableValue(expression, context);
12052
+ if (isFunctionLike$2(resolvedExpression)) return resolvedExpression;
12053
+ if (!isNodeOfType(resolvedExpression, "CallExpression") || !isReactApiCall(resolvedExpression, "useCallback", context.scopes)) return null;
12054
+ return getEffectCallback(resolvedExpression);
12055
+ };
12056
+ const findRefOwnedHandlerStorage = (retainedFunction, usage, context) => {
12057
+ if (!isFunctionLike$2(retainedFunction) || usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.handlerKey === null || !usage.receiverKey?.startsWith("global:") || !usage.eventKey?.startsWith("literal:")) return null;
12058
+ const usageStart = getRangeStart(usage.node);
12059
+ const functionCfg = context.cfg.cfgFor(retainedFunction);
12060
+ const usageBlock = functionCfg?.blockOf(usage.node);
12061
+ if (usageStart === null || !functionCfg || !usageBlock) return null;
12062
+ const matchingStorage = [];
12063
+ walkAst(retainedFunction.body, (child) => {
12064
+ if (child !== retainedFunction.body && isFunctionLike$2(child)) return false;
12065
+ if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=") return;
12066
+ const assignmentStart = getRangeStart(child);
12067
+ const refCurrentExpression = stripParenExpression(child.left);
12068
+ if (assignmentStart === null || assignmentStart >= usageStart || functionCfg.blockOf(child) !== usageBlock || !isNodeOfType(refCurrentExpression, "MemberExpression") || !resolveReactRefSymbol(refCurrentExpression, context.scopes)) return;
12069
+ const refCurrentKey = resolveExpressionKey(refCurrentExpression, context);
12070
+ const refKey = resolveExpressionKey(refCurrentExpression.object, context);
12071
+ const storedSession = stripParenExpression(child.right);
12072
+ if (!refCurrentKey || !refKey || !isNodeOfType(storedSession, "ObjectExpression")) return;
12073
+ const storedSessionProperties = storedSession.properties ?? [];
12074
+ if (storedSessionProperties.some((property) => !isNodeOfType(property, "Property"))) return;
12075
+ for (const property of storedSessionProperties) {
12076
+ if (!isNodeOfType(property, "Property")) continue;
12077
+ const propertyName = getStaticPropertyKeyName(property);
12078
+ if (propertyName && resolveExpressionKey(property.value, context) === usage.handlerKey) {
12079
+ const handlerKey = `${refCurrentKey}.${propertyName}`;
12080
+ matchingStorage.push({
12081
+ handlerKey,
12082
+ refCurrentKey,
12083
+ refKey,
12084
+ assignmentNode: child
12085
+ });
12086
+ }
12087
+ }
12088
+ });
12089
+ return matchingStorage.length === 1 ? matchingStorage[0] : null;
12090
+ };
12091
+ const doMatchingNodesCoverEveryPathBeforeUsage = (usageNode, matchingNodes, owner, context) => {
12092
+ const functionCfg = context.cfg.cfgFor(owner);
12093
+ const usageBlock = functionCfg?.blockOf(usageNode);
12094
+ const usageStart = getRangeStart(usageNode);
12095
+ if (!functionCfg || !usageBlock || usageStart === null) return false;
12096
+ const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
12097
+ if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
12098
+ const matchingStart = getRangeStart(matchingNode);
12099
+ if (matchingStart === null || matchingStart >= usageStart) return [];
12100
+ const matchingBlock = functionCfg.blockOf(matchingNode);
12101
+ return matchingBlock ? [matchingBlock] : [];
12102
+ }));
12103
+ if (matchingBlocks.size === 0) return false;
12104
+ if (matchingBlocks.has(usageBlock)) return true;
12105
+ const visitedBlocks = new Set([functionCfg.entry]);
12106
+ const pendingBlocks = [functionCfg.entry];
12107
+ while (pendingBlocks.length > 0) {
12108
+ const currentBlock = pendingBlocks.pop();
12109
+ if (!currentBlock) break;
12110
+ if (matchingBlocks.has(currentBlock)) continue;
12111
+ if (currentBlock === usageBlock) return false;
12112
+ for (const edge of currentBlock.successors) {
12113
+ if (visitedBlocks.has(edge.to)) continue;
12114
+ visitedBlocks.add(edge.to);
12115
+ pendingBlocks.push(edge.to);
12116
+ }
12117
+ }
12118
+ return true;
12119
+ };
12120
+ const retainedFunctionReleasesPreviousRefOwnedUsage = (retainedFunction, cleanupFunction, storageNode, context) => {
12121
+ if (!isFunctionLike$2(retainedFunction)) return false;
12122
+ const retainedFunctionBody = retainedFunction.body;
12123
+ const cleanupCalls = [];
12124
+ walkAst(retainedFunctionBody, (child) => {
12125
+ if (child !== retainedFunctionBody && isFunctionLike$2(child)) return false;
12126
+ if (isNodeOfType(child, "CallExpression") && resolveRefOwnedCleanupFunction(child.callee, context) === cleanupFunction) cleanupCalls.push(child);
12127
+ });
12128
+ return doMatchingNodesCoverEveryPathBeforeUsage(storageNode, cleanupCalls, retainedFunction, context);
12129
+ };
12130
+ const isDirectRefOwnedRelease = (releaseNode, cleanupFunction, usage, storedHandlerKey, refCurrentKey, context) => {
12131
+ if (!isFunctionLike$2(cleanupFunction)) return false;
12132
+ const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
12133
+ if (!isNodeOfType(releaseCall, "CallExpression") || !isNodeOfType(releaseCall.callee, "MemberExpression") || releaseCall.callee.computed || !isNodeOfType(releaseCall.callee.property, "Identifier") || releaseCall.callee.property.name !== "removeEventListener" || !isNodeOfType(usage.node, "CallExpression") || usage.node.arguments?.[2] !== void 0 || releaseCall.arguments?.[2] !== void 0) return false;
12134
+ const releaseStatement = findTransparentExpressionRoot(releaseNode).parent;
12135
+ const releaseBlock = releaseStatement?.parent;
12136
+ const releaseGuard = releaseBlock?.parent;
12137
+ const isDirectCleanupStatement = releaseBlock === cleanupFunction.body;
12138
+ const isRefPresenceGuardedStatement = Boolean(isNodeOfType(releaseBlock, "BlockStatement") && isNodeOfType(releaseGuard, "IfStatement") && releaseGuard.consequent === releaseBlock && releaseGuard.alternate === null && resolveExpressionKey(releaseGuard.test, context) === refCurrentKey);
12139
+ if (!isNodeOfType(releaseStatement, "ExpressionStatement") || !isNodeOfType(cleanupFunction.body, "BlockStatement") || !isDirectCleanupStatement && !isRefPresenceGuardedStatement) return false;
12140
+ return usage.receiverKey !== null && usage.receiverKey === resolveExpressionKey(releaseCall.callee.object, context) && usage.eventKey !== null && usage.eventKey === resolveExpressionKey(releaseCall.arguments?.[0], context) && resolveExpressionKey(releaseCall.arguments?.[1], context) === storedHandlerKey;
12141
+ };
12142
+ const isRefPresenceGuardedEarlyReturn = (returnStatement, refCurrentKey, context) => {
12143
+ const returnBranch = returnStatement.parent;
12144
+ const guardStatement = isNodeOfType(returnBranch, "BlockStatement") ? returnBranch.parent : returnBranch;
12145
+ const guardedConsequent = isNodeOfType(returnBranch, "BlockStatement") ? returnBranch : returnStatement;
12146
+ if (!isNodeOfType(guardStatement, "IfStatement") || guardStatement.consequent !== guardedConsequent || guardStatement.alternate !== null) return false;
12147
+ const guardTest = stripParenExpression(guardStatement.test);
12148
+ return isNodeOfType(guardTest, "UnaryExpression") && guardTest.operator === "!" && resolveExpressionKey(guardTest.argument, context) === refCurrentKey;
12149
+ };
12150
+ const hasUnprovenReturnBeforeRefOwnedRelease = (cleanupFunction, releaseNode, refCurrentKey, context) => {
12151
+ if (!isFunctionLike$2(cleanupFunction)) return true;
12152
+ const releaseStart = getRangeStart(releaseNode);
12153
+ if (releaseStart === null) return true;
12154
+ let hasUnprovenEarlyReturn = false;
12155
+ walkAst(cleanupFunction.body, (child) => {
12156
+ if (hasUnprovenEarlyReturn) return false;
12157
+ if (child !== cleanupFunction.body && isFunctionLike$2(child)) return false;
12158
+ if (!isNodeOfType(child, "ReturnStatement")) return;
12159
+ const returnStart = getRangeStart(child);
12160
+ if ((returnStart === null || returnStart < releaseStart) && !isRefPresenceGuardedEarlyReturn(child, refCurrentKey, context)) {
12161
+ hasUnprovenEarlyReturn = true;
12162
+ return false;
12163
+ }
12164
+ });
12165
+ return hasUnprovenEarlyReturn;
12166
+ };
12167
+ const cleanupFunctionReleasesRefOwnedUsage = (cleanupFunction, componentFunction, retainedFunction, usage, context) => {
12168
+ if (!isFunctionLike$2(cleanupFunction) || !isFunctionLike$2(componentFunction) || cleanupFunction.async || cleanupFunction.generator) return false;
12169
+ const storage = findRefOwnedHandlerStorage(retainedFunction, usage, context);
12170
+ if (!storage) return false;
12171
+ if (!retainedFunctionReleasesPreviousRefOwnedUsage(retainedFunction, cleanupFunction, storage.assignmentNode, context)) return false;
12172
+ let releaseNode = null;
12173
+ walkAst(cleanupFunction.body, (child) => {
12174
+ if (releaseNode) return false;
12175
+ if (child !== cleanupFunction.body && isFunctionLike$2(child)) return false;
12176
+ if (isDirectRefOwnedRelease(child, cleanupFunction, usage, storage.handlerKey, storage.refCurrentKey, context)) {
12177
+ releaseNode = child;
12178
+ return false;
12179
+ }
12180
+ });
12181
+ if (!releaseNode) return false;
12182
+ if (hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, releaseNode, storage.refCurrentKey, context)) return false;
12183
+ let hasUnsafeRefWrite = false;
12184
+ walkAst(componentFunction.body, (child) => {
12185
+ if (hasUnsafeRefWrite) return false;
12186
+ if (isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
12187
+ const deleteTarget = stripParenExpression(child.argument);
12188
+ if (resolveExpressionKey(deleteTarget, context) === storage.handlerKey || isNodeOfType(deleteTarget, "MemberExpression") && resolveExpressionKey(deleteTarget.object, context) === storage.refCurrentKey) {
12189
+ hasUnsafeRefWrite = true;
12190
+ return false;
12191
+ }
12192
+ return;
12193
+ }
12194
+ if (isNodeOfType(child, "CallExpression")) {
12195
+ if ((child.arguments ?? []).some((argumentNode) => {
12196
+ const argumentKey = resolveExpressionKey(argumentNode, context);
12197
+ return argumentKey !== null && (argumentKey === storage.refKey || argumentKey === storage.refCurrentKey);
12198
+ })) {
12199
+ hasUnsafeRefWrite = true;
12200
+ return false;
12201
+ }
12202
+ return;
12203
+ }
12204
+ if (!isNodeOfType(child, "AssignmentExpression")) return;
12205
+ const assignmentTarget = stripParenExpression(child.left);
12206
+ if (isNodeOfType(assignmentTarget, "MemberExpression") && assignmentTarget.computed && resolveExpressionKey(assignmentTarget.object, context) === storage.refCurrentKey) {
12207
+ hasUnsafeRefWrite = true;
12208
+ return false;
12209
+ }
12210
+ const assignedKey = resolveExpressionKey(child.left, context);
12211
+ if (assignedKey === storage.handlerKey) {
12212
+ hasUnsafeRefWrite = true;
12213
+ return false;
12214
+ }
12215
+ if (assignedKey !== storage.refCurrentKey) return;
12216
+ const assignedValue = stripParenExpression(child.right);
12217
+ if (isNodeOfType(assignedValue, "Literal") && assignedValue.value === null && findEnclosingFunction(child) === cleanupFunction) return;
12218
+ const assignedSessionProperties = (isNodeOfType(assignedValue, "ObjectExpression") ? assignedValue : null)?.properties ?? [];
12219
+ const storesMatchingHandler = assignedSessionProperties.every((property) => isNodeOfType(property, "Property")) && assignedSessionProperties.some((property) => isNodeOfType(property, "Property") && `${storage.refCurrentKey}.${getStaticPropertyKeyName(property) ?? ""}` === storage.handlerKey && resolveExpressionKey(property.value, context) === usage.handlerKey);
12220
+ if (findEnclosingFunction(child) !== retainedFunction || !storesMatchingHandler) {
12221
+ hasUnsafeRefWrite = true;
12222
+ return false;
12223
+ }
12224
+ });
12225
+ return !hasUnsafeRefWrite;
12226
+ };
12227
+ const effectReturnsRefOwnedCleanup = (effectCallback, componentFunction, retainedFunction, usage, context) => {
12228
+ const matchesReturnedCleanup = (returnedValue) => {
12229
+ const cleanupFunction = resolveRefOwnedCleanupFunction(returnedValue, context);
12230
+ return Boolean(cleanupFunction && cleanupFunctionReleasesRefOwnedUsage(cleanupFunction, componentFunction, retainedFunction, usage, context));
12231
+ };
12232
+ if (!isFunctionLike$2(effectCallback)) return false;
12233
+ if (!isNodeOfType(effectCallback.body, "BlockStatement")) return matchesReturnedCleanup(stripParenExpression(effectCallback.body));
12234
+ const matchingReturns = [];
12235
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
12236
+ if (isNodeOfType(child, "ReturnStatement") && child.argument && matchesReturnedCleanup(stripParenExpression(child.argument))) matchingReturns.push(child);
12237
+ });
12238
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
12239
+ };
12240
+ const hasGuaranteedRefOwnedUnmountCleanup = (retainedFunction, usage, context) => {
12241
+ const componentFunction = findEnclosingFunction(retainedFunction);
12242
+ if (!componentFunction || !isFunctionLike$2(componentFunction)) return false;
12243
+ let didFindCleanupEffect = false;
12244
+ walkAst(componentFunction.body, (child) => {
12245
+ if (didFindCleanupEffect) return false;
12246
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
12247
+ const effectStatement = findTransparentExpressionRoot(child).parent;
12248
+ if (!isNodeOfType(effectStatement, "ExpressionStatement") || effectStatement.parent !== componentFunction.body) return;
12249
+ const effectCallback = getEffectCallback(child);
12250
+ if (effectCallback && effectReturnsRefOwnedCleanup(effectCallback, componentFunction, retainedFunction, usage, context)) {
12251
+ didFindCleanupEffect = true;
12252
+ return false;
12253
+ }
12254
+ });
12255
+ return didFindCleanupEffect;
12256
+ };
11724
12257
  const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
11725
12258
  const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
11726
12259
  if (!bindingIdentifier) return false;
@@ -11761,7 +12294,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
11761
12294
  let leak = null;
11762
12295
  const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
11763
12296
  const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
11764
- const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
12297
+ const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context) || hasGuaranteedRefOwnedUnmountCleanup(retainedFunction, usage, context);
11765
12298
  walkAst(body, (child) => {
11766
12299
  if (leak !== null) return false;
11767
12300
  if (isFunctionLike$2(child)) return false;
@@ -12970,7 +13503,7 @@ const stringifyMemberChain = (node) => {
12970
13503
  }
12971
13504
  return null;
12972
13505
  };
12973
- const collectCaptureDepKeys = (callback, scopes) => {
13506
+ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys) => {
12974
13507
  const keys = /* @__PURE__ */ new Set();
12975
13508
  const stableCapturedNames = /* @__PURE__ */ new Set();
12976
13509
  const moduleScopeCapturedNames = /* @__PURE__ */ new Set();
@@ -13000,6 +13533,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
13000
13533
  continue;
13001
13534
  }
13002
13535
  if (depKey === symbol.name) {
13536
+ if (declaredExactBindingKeys?.has(depKey)) {
13537
+ keys.add(depKey);
13538
+ continue;
13539
+ }
13003
13540
  const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
13004
13541
  if (identitySourceKeys) {
13005
13542
  if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
@@ -13108,13 +13645,13 @@ const isRegExpLiteral = (node) => {
13108
13645
  if (!isNodeOfType(node, "Literal")) return false;
13109
13646
  return Boolean(node.regex);
13110
13647
  };
13111
- const isUnstableInitializer = (node) => {
13648
+ const isUnstableInitializer = (node, isNestedInitializer = false) => {
13112
13649
  if (!node) return false;
13113
13650
  const stripped = unwrapExpression$3(node);
13114
13651
  if (isRegExpLiteral(stripped)) return true;
13115
- if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
13116
- if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
13117
- return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
13652
+ if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent, true) || isUnstableInitializer(stripped.alternate, true);
13653
+ if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left, true) || isUnstableInitializer(stripped.right, true);
13654
+ return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNestedInitializer && (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression")) || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
13118
13655
  };
13119
13656
  const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
13120
13657
  const candidate = unwrapExpression$3(node);
@@ -13567,8 +14104,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
13567
14104
  });
13568
14105
  return;
13569
14106
  }
13570
- const { keys: captureKeys, stableCapturedNames, moduleScopeCapturedNames, outerFunctionCapturedNames } = collectCaptureDepKeys(callbackToAnalyze ?? callbackArgument, context.scopes);
13571
- for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
13572
14107
  const hasLiteralDepElement = depsArgument.elements.some((element) => {
13573
14108
  if (!element) return false;
13574
14109
  return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
@@ -13582,6 +14117,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
13582
14117
  message: buildLiteralDepMessage(hookName)
13583
14118
  });
13584
14119
  const declaredKeys = /* @__PURE__ */ new Set();
14120
+ const declaredExactBindingKeys = /* @__PURE__ */ new Set();
13585
14121
  const declaredKeyToReportNode = /* @__PURE__ */ new Map();
13586
14122
  const seenDeclaredKeys = /* @__PURE__ */ new Set();
13587
14123
  let didReportRefCurrentDep = false;
@@ -13647,8 +14183,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
13647
14183
  }
13648
14184
  seenDeclaredKeys.add(key);
13649
14185
  declaredKeys.add(key);
14186
+ if (isNodeOfType(stripped, "Identifier")) declaredExactBindingKeys.add(key);
13650
14187
  declaredKeyToReportNode.set(key, elementNode);
13651
14188
  }
14189
+ const { keys: captureKeys, stableCapturedNames, moduleScopeCapturedNames, outerFunctionCapturedNames } = collectCaptureDepKeys(callbackToAnalyze ?? callbackArgument, context.scopes, declaredExactBindingKeys);
14190
+ for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
13652
14191
  addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
13653
14192
  const missingCaptureKeys = [];
13654
14193
  for (const captureKey of captureKeys) {
@@ -15646,7 +16185,7 @@ const jotaiTqUseRawQueryAtom = defineRule({
15646
16185
  });
15647
16186
  //#endregion
15648
16187
  //#region src/plugin/rules/js-performance/js-async-reduce-without-awaited-acc.ts
15649
- const isAsyncFunctionLike$1 = (node) => {
16188
+ const isAsyncFunctionLike = (node) => {
15650
16189
  if (!node) return false;
15651
16190
  if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
15652
16191
  return node.async === true;
@@ -15713,7 +16252,7 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
15713
16252
  const args = node.arguments ?? [];
15714
16253
  if (args.length === 0) return;
15715
16254
  const reducerCandidate = stripParenExpression(args[0]);
15716
- if (!isAsyncFunctionLike$1(reducerCandidate)) return;
16255
+ if (!isAsyncFunctionLike(reducerCandidate)) return;
15717
16256
  const reducer = reducerCandidate;
15718
16257
  if (!containsDirectAwait(reducer.body)) return;
15719
16258
  const firstParameter = classifyFirstParameter(reducer);
@@ -16997,30 +17536,6 @@ const jsIndexMaps = defineRule({
16997
17536
  } })
16998
17537
  });
16999
17538
  //#endregion
17000
- //#region src/plugin/utils/are-expressions-structurally-equal.ts
17001
- const areExpressionsStructurallyEqual = (a, b) => {
17002
- if (!a || !b) return a === b;
17003
- const unwrappedA = stripParenExpression(a);
17004
- const unwrappedB = stripParenExpression(b);
17005
- if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
17006
- if (a.type !== b.type) return false;
17007
- if (isNodeOfType(a, "ThisExpression")) return true;
17008
- if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
17009
- if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
17010
- if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
17011
- if (a.computed !== b.computed) return false;
17012
- return areExpressionsStructurallyEqual(a.object, b.object) && areExpressionsStructurallyEqual(a.property, b.property);
17013
- }
17014
- if (isNodeOfType(a, "CallExpression") && isNodeOfType(b, "CallExpression")) {
17015
- if (!areExpressionsStructurallyEqual(a.callee, b.callee)) return false;
17016
- const argumentsA = a.arguments ?? [];
17017
- const argumentsB = b.arguments ?? [];
17018
- if (argumentsA.length !== argumentsB.length) return false;
17019
- return argumentsA.every((argument, index) => areExpressionsStructurallyEqual(argument, argumentsB[index]));
17020
- }
17021
- return false;
17022
- };
17023
- //#endregion
17024
17539
  //#region src/plugin/utils/flatten-logical-and-chain.ts
17025
17540
  const flattenLogicalAndChain = (node) => {
17026
17541
  if (isNodeOfType(node, "LogicalExpression") && node.operator === "&&") return [...flattenLogicalAndChain(node.left), ...flattenLogicalAndChain(node.right)];
@@ -28299,271 +28814,21 @@ const noCallComponentAsFunction = defineRule({
28299
28814
  }
28300
28815
  });
28301
28816
  //#endregion
28302
- //#region src/plugin/utils/is-setter-identifier.ts
28303
- const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
28304
- //#endregion
28305
- //#region src/plugin/utils/is-setter-call.ts
28306
- const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
28307
- //#endregion
28308
- //#region src/plugin/utils/is-hook-binding-in-scope.ts
28309
- const isHookBindingInScope = (node, query) => {
28310
- const { bindingName, hookName, destructureIndex } = query;
28311
- let cursor = node;
28312
- while (cursor) {
28313
- if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
28314
- if (!isNodeOfType(statement, "VariableDeclaration")) continue;
28315
- for (const declarator of statement.declarations ?? []) {
28316
- if (!isNodeOfType(declarator.init, "CallExpression")) continue;
28317
- if (!isHookCall$2(declarator.init, hookName)) continue;
28318
- if (destructureIndex !== void 0) {
28319
- if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
28320
- const elements = declarator.id.elements ?? [];
28321
- if (elements.length <= destructureIndex) continue;
28322
- const element = elements[destructureIndex];
28323
- if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
28324
- } else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
28325
- }
28326
- }
28327
- cursor = cursor.parent ?? null;
28328
- }
28329
- return false;
28330
- };
28331
- //#endregion
28332
- //#region src/plugin/utils/is-use-state-setter-in-scope.ts
28333
- const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
28334
- bindingName: setterName,
28335
- hookName: "useState",
28336
- destructureIndex: 1
28817
+ //#region src/plugin/utils/define-retired-rule.ts
28818
+ const defineRetiredRule = (rule) => defineRule({
28819
+ ...rule,
28820
+ defaultEnabled: false,
28821
+ lifecycle: "retired",
28822
+ create: () => ({})
28337
28823
  });
28338
28824
  //#endregion
28339
28825
  //#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
28340
- const isAsyncFunctionLike = (node) => {
28341
- if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
28342
- return false;
28343
- };
28344
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
28345
- "forEach",
28346
- "map",
28347
- "filter",
28348
- "reduce",
28349
- "reduceRight",
28350
- "flatMap",
28351
- "some",
28352
- "every",
28353
- "find",
28354
- "findIndex",
28355
- "findLast",
28356
- "findLastIndex",
28357
- "sort"
28358
- ]);
28359
- const runsOnEffectDispatch = (functionNode) => {
28360
- const parent = functionNode.parent;
28361
- if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
28362
- if (parent.callee === functionNode) return true;
28363
- if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
28364
- const callee = parent.callee;
28365
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
28366
- };
28367
- const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
28368
- const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
28369
- const analyzeStatementSequence = (statements, context) => {
28370
- let fallThroughCount = 0;
28371
- let maxTerminatedCount = 0;
28372
- for (const statement of statements) {
28373
- if (isNodeOfType(statement, "IfStatement")) {
28374
- fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
28375
- const thenSummary = analyzeBranchStatements(statement.consequent, context);
28376
- const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
28377
- fallThroughCount: 0,
28378
- maxTerminatedCount: 0,
28379
- doAllPathsTerminate: false
28380
- };
28381
- maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
28382
- if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
28383
- fallThroughCount: 0,
28384
- maxTerminatedCount,
28385
- doAllPathsTerminate: true
28386
- };
28387
- const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
28388
- fallThroughCount += Math.max(...fallThroughBranchCounts);
28389
- continue;
28390
- }
28391
- if (isTerminatingStatement(statement)) {
28392
- const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
28393
- return {
28394
- fallThroughCount: 0,
28395
- maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
28396
- doAllPathsTerminate: true
28397
- };
28398
- }
28399
- fallThroughCount += countMaxPathSetStateCalls(statement, context);
28400
- }
28401
- return {
28402
- fallThroughCount,
28403
- maxTerminatedCount,
28404
- doAllPathsTerminate: false
28405
- };
28406
- };
28407
- const countStatementSequenceSetStateCalls = (statements, context) => {
28408
- const summary = analyzeStatementSequence(statements, context);
28409
- return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
28410
- };
28411
- const collectLocalHelperFunctions = (root) => {
28412
- const helpersByName = /* @__PURE__ */ new Map();
28413
- const visit = (node) => {
28414
- if (isNodeOfType(node, "FunctionDeclaration") && node.id) helpersByName.set(node.id.name, node);
28415
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isFunctionLike$2(node.init)) helpersByName.set(node.id.name, node.init);
28416
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isNodeOfType(node.init, "CallExpression") && isNodeOfType(node.init.callee, "Identifier") && node.init.callee.name === "useCallback" && node.init.arguments?.[0] && isFunctionLike$2(node.init.arguments[0])) helpersByName.set(node.id.name, node.init.arguments[0]);
28417
- const record = node;
28418
- for (const key of Object.keys(record)) {
28419
- if (key === "parent") continue;
28420
- const child = record[key];
28421
- if (Array.isArray(child)) {
28422
- for (const item of child) if (item && typeof item === "object" && "type" in item) visit(item);
28423
- } else if (child && typeof child === "object" && "type" in child) visit(child);
28424
- }
28425
- };
28426
- visit(root);
28427
- return helpersByName;
28428
- };
28429
- const isWholesaleDelegationCall = (callNode, effectCallback) => {
28430
- const parent = callNode.parent;
28431
- if (!parent) return false;
28432
- if (parent === effectCallback) return true;
28433
- if (!isNodeOfType(parent, "ExpressionStatement")) return false;
28434
- return parent.parent === effectCallback.body;
28435
- };
28436
- const countFunctionBodySetStateCalls = (functionNode, context) => {
28437
- if (isAsyncFunctionLike(functionNode)) return 0;
28438
- const body = functionNode.body;
28439
- if (!body) return 0;
28440
- return countMaxPathSetStateCalls(body, context);
28441
- };
28442
- const countMaxPathSetStateCalls = (node, context) => {
28443
- if (!node || typeof node !== "object") return 0;
28444
- if (isFunctionLike$2(node)) return 0;
28445
- if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
28446
- if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
28447
- const consequent = node.consequent;
28448
- const alternate = node.alternate;
28449
- const testCount = countMaxPathSetStateCalls(node.test, context);
28450
- const thenCount = countMaxPathSetStateCalls(consequent, context);
28451
- const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
28452
- return testCount + Math.max(thenCount, elseCount);
28453
- }
28454
- if (isNodeOfType(node, "SwitchStatement")) {
28455
- let maxRunSetters = 0;
28456
- let currentRunSetters = 0;
28457
- for (const switchCase of node.cases ?? []) {
28458
- const consequent = switchCase.consequent ?? [];
28459
- let caseSetters = 0;
28460
- let runEnds = false;
28461
- for (const statement of consequent) {
28462
- caseSetters += countMaxPathSetStateCalls(statement, context);
28463
- if (isTerminatingStatement(statement)) runEnds = true;
28464
- }
28465
- currentRunSetters += caseSetters;
28466
- if (runEnds) {
28467
- if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
28468
- currentRunSetters = 0;
28469
- }
28470
- }
28471
- if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
28472
- return maxRunSetters;
28473
- }
28474
- if (isNodeOfType(node, "TryStatement")) {
28475
- const tryCount = countMaxPathSetStateCalls(node.block, context);
28476
- const catchCount = node.handler ? countMaxPathSetStateCalls(node.handler.body, context) : 0;
28477
- const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer, context) : 0;
28478
- return Math.max(tryCount, catchCount) + finallyCount;
28479
- }
28480
- if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
28481
- let nestedSettersInArgs = 0;
28482
- for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$2(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
28483
- return 1 + nestedSettersInArgs;
28484
- }
28485
- if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
28486
- const helperFunction = context.helpersByName.get(node.callee.name);
28487
- if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
28488
- context.activeHelpers.add(helperFunction);
28489
- let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
28490
- context.activeHelpers.delete(helperFunction);
28491
- for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
28492
- return helperCount;
28493
- }
28494
- }
28495
- const countChild = (child) => {
28496
- if (isFunctionLike$2(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
28497
- return countMaxPathSetStateCalls(child, context);
28498
- };
28499
- let total = 0;
28500
- const nodeRecord = node;
28501
- for (const key of Object.keys(nodeRecord)) {
28502
- if (key === "parent") continue;
28503
- const child = nodeRecord[key];
28504
- if (Array.isArray(child)) {
28505
- for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
28506
- } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
28507
- }
28508
- return total;
28509
- };
28510
- const isInitOnlyEffect = (node) => {
28511
- const depsArg = node.arguments?.[1];
28512
- if (!depsArg) return false;
28513
- if (!isNodeOfType(depsArg, "ArrayExpression")) return false;
28514
- return (depsArg.elements ?? []).length === 0;
28515
- };
28516
- const DEV_ENV_FLAG_NAMES = new Set([
28517
- "DEV",
28518
- "PROD",
28519
- "MODE",
28520
- "NODE_ENV"
28521
- ]);
28522
- const mentionsDevEnvFlag = (node) => {
28523
- if (!node || typeof node !== "object") return false;
28524
- if (isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && DEV_ENV_FLAG_NAMES.has(node.property.name) && isNodeOfType(node.object, "MemberExpression") && isNodeOfType(node.object.property, "Identifier") && node.object.property.name === "env") return true;
28525
- const record = node;
28526
- for (const key of Object.keys(record)) {
28527
- if (key === "parent") continue;
28528
- const child = record[key];
28529
- if (Array.isArray(child)) {
28530
- for (const item of child) if (item && typeof item === "object" && "type" in item && mentionsDevEnvFlag(item)) return true;
28531
- } else if (child && typeof child === "object" && "type" in child && mentionsDevEnvFlag(child)) return true;
28532
- }
28533
- return false;
28534
- };
28535
- const isDevOnlyGuardedEffect = (callback) => {
28536
- const body = callback.body;
28537
- if (!body || !isNodeOfType(body, "BlockStatement")) return false;
28538
- const firstStatement = (body.body ?? [])[0];
28539
- if (!firstStatement) return false;
28540
- if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
28541
- const consequent = firstStatement.consequent;
28542
- if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
28543
- return mentionsDevEnvFlag(firstStatement.test);
28544
- };
28545
- const noCascadingSetState = defineRule({
28826
+ const noCascadingSetState = defineRetiredRule({
28546
28827
  id: "no-cascading-set-state",
28547
28828
  title: "Multiple setState calls in one effect",
28548
28829
  severity: "warn",
28549
28830
  tags: ["test-noise"],
28550
- recommendation: "Combine related updates in `useReducer` so one effect does not redraw the screen once per `setState` call.",
28551
- create: (context) => ({ CallExpression(node) {
28552
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
28553
- if (isInitOnlyEffect(node)) return;
28554
- const callback = getEffectCallback(node, context.scopes);
28555
- if (!callback) return;
28556
- if (isDevOnlyGuardedEffect(callback)) return;
28557
- const setStateCallCount = countFunctionBodySetStateCalls(callback, {
28558
- helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
28559
- activeHelpers: /* @__PURE__ */ new Set(),
28560
- effectCallback: callback
28561
- });
28562
- if (setStateCallCount >= 3) context.report({
28563
- node,
28564
- message: `${setStateCallCount} setState calls in one useEffect redraw your screen each time they run together.`
28565
- });
28566
- } })
28831
+ recommendation: "Retired: React batches synchronous state updates from one effect into the same follow-up commit, so setter count does not prove repeated redraws."
28567
28832
  });
28568
28833
  //#endregion
28569
28834
  //#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
@@ -30437,6 +30702,9 @@ const noDirectMutationState = defineRule({
30437
30702
  })
30438
30703
  });
30439
30704
  //#endregion
30705
+ //#region src/plugin/utils/is-setter-identifier.ts
30706
+ const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
30707
+ //#endregion
30440
30708
  //#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
30441
30709
  const collectUseStateBindings = (componentBody) => {
30442
30710
  const bindings = [];
@@ -31323,7 +31591,7 @@ const noEffectChain = defineRule({
31323
31591
  const effectInfos = [];
31324
31592
  for (const effectCall of findTopLevelEffectCalls(componentBody)) {
31325
31593
  const callback = getEffectCallback(effectCall, context.scopes);
31326
- if (!callback) continue;
31594
+ if (!callback || !isFunctionLike$2(callback) || callback.async) continue;
31327
31595
  const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
31328
31596
  const writtenStateNames = collectWrittenStateNamesInEffect(analysisFunctions, setterToStateName);
31329
31597
  effectInfos.push({
@@ -32707,6 +32975,9 @@ const noEventTriggerState = defineRule({
32707
32975
  }
32708
32976
  });
32709
32977
  //#endregion
32978
+ //#region src/plugin/utils/is-setter-call.ts
32979
+ const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
32980
+ //#endregion
32710
32981
  //#region src/plugin/rules/state-and-effects/no-fetch-in-effect.ts
32711
32982
  const IMPORT_INITIALIZER_TYPES = new Set([
32712
32983
  "ImportSpecifier",
@@ -33954,26 +34225,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
33954
34225
  } })
33955
34226
  });
33956
34227
  //#endregion
33957
- //#region src/plugin/utils/react-ref-origin.ts
33958
- const resolveReactRefSymbol = (memberExpression, scopes) => {
33959
- const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
33960
- if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
33961
- const symbol = resolveConstIdentifierAlias(receiver, scopes);
33962
- if (!symbol?.initializer) return null;
33963
- const initializer = stripParenExpression(symbol.initializer);
33964
- if (!isNodeOfType(initializer, "CallExpression")) return null;
33965
- return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
33966
- };
33967
- const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
33968
- const expression = stripParenExpression(node);
33969
- if (resolveReactRefSymbol(expression, scopes)) return true;
33970
- if (!isNodeOfType(expression, "Identifier")) return false;
33971
- const symbol = resolveConstIdentifierAlias(expression, scopes);
33972
- if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
33973
- visitedSymbolIds.add(symbol.id);
33974
- return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
33975
- };
33976
- //#endregion
33977
34228
  //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
33978
34229
  const TIMER_FUNCTION_NAMES = new Set([
33979
34230
  "cancelAnimationFrame",
@@ -40937,10 +41188,10 @@ const noStaticElementInteractions = defineRule({
40937
41188
  const isTestlikeFile = isTestlikeFilename(context.filename);
40938
41189
  return { JSXOpeningElement(node) {
40939
41190
  if (isTestlikeFile) return;
40940
- let hasNonBlockerHandler = false;
40941
41191
  let hasAnyHandler = false;
40942
41192
  let isKeyboardTarget = null;
40943
41193
  let seenHandlerNames = null;
41194
+ let nonBlockerHandlerNamesLower = null;
40944
41195
  for (const attribute of node.attributes) {
40945
41196
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
40946
41197
  const attributeName = getJsxAttributeName(attribute.name);
@@ -40955,13 +41206,12 @@ const noStaticElementInteractions = defineRule({
40955
41206
  if (!isKeyboardTarget) continue;
40956
41207
  }
40957
41208
  hasAnyHandler = true;
40958
- if (!isPureEventBlockerHandler(attribute)) {
40959
- hasNonBlockerHandler = true;
40960
- break;
40961
- }
41209
+ if (!isPureEventBlockerHandler(attribute)) (nonBlockerHandlerNamesLower ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
40962
41210
  }
40963
41211
  if (!hasAnyHandler) return;
40964
- if (!hasNonBlockerHandler) return;
41212
+ if (!nonBlockerHandlerNamesLower) return;
41213
+ const onClick = nonBlockerHandlerNamesLower.size === 1 && nonBlockerHandlerNamesLower.has("onclick") ? hasJsxPropIgnoreCase(node.attributes, "onClick") : null;
41214
+ if (onClick && hasKeyboardActivatableDescendant(node.parent, onClick, context.scopes, context.settings)) return;
40965
41215
  const elementType = getElementType(node, context.settings);
40966
41216
  if (!HTML_TAGS.has(elementType)) return;
40967
41217
  if (elementType === "svg") return;
@@ -45418,19 +45668,63 @@ const preferTagOverRole = defineRule({
45418
45668
  });
45419
45669
  //#endregion
45420
45670
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
45421
- const collectFunctionTypedLocalBindings = (componentBody) => {
45422
- const functionTypedLocals = /* @__PURE__ */ new Set();
45423
- if (!isNodeOfType(componentBody, "BlockStatement")) return functionTypedLocals;
45671
+ const STABLE_REACT_HOOK_VALUE_NAMES = new Set([
45672
+ "useActionState",
45673
+ "useEffectEvent",
45674
+ "useReducer",
45675
+ "useRef",
45676
+ "useState",
45677
+ "useTransition"
45678
+ ]);
45679
+ const isStableReactHookDependency = (dependency, context) => {
45680
+ const unwrappedDependency = stripParenExpression(dependency);
45681
+ if (!isNodeOfType(unwrappedDependency, "Identifier")) return false;
45682
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
45683
+ let dependencySymbol = context.scopes.symbolFor(unwrappedDependency);
45684
+ while (dependencySymbol) {
45685
+ if (visitedSymbolIds.has(dependencySymbol.id)) return false;
45686
+ visitedSymbolIds.add(dependencySymbol.id);
45687
+ if (symbolHasStableHookOrigin(dependencySymbol, context.scopes)) {
45688
+ let declarator = dependencySymbol.declarationNode;
45689
+ while (declarator && !isNodeOfType(declarator, "VariableDeclarator")) declarator = declarator.parent;
45690
+ if (!declarator?.init) return false;
45691
+ return isReactApiCall(stripParenExpression(declarator.init), STABLE_REACT_HOOK_VALUE_NAMES, context.scopes, {
45692
+ allowGlobalReactNamespace: true,
45693
+ resolveNamedAliases: true
45694
+ });
45695
+ }
45696
+ if (dependencySymbol.kind !== "const" || dependencySymbol.references.some((reference) => reference.flag !== "read") || !isNodeOfType(dependencySymbol.declarationNode, "VariableDeclarator") || dependencySymbol.declarationNode.id !== dependencySymbol.bindingIdentifier || !dependencySymbol.initializer) return false;
45697
+ const aliasInitializer = stripParenExpression(dependencySymbol.initializer);
45698
+ if (!isNodeOfType(aliasInitializer, "Identifier")) return false;
45699
+ dependencySymbol = context.scopes.symbolFor(aliasInitializer);
45700
+ }
45701
+ return false;
45702
+ };
45703
+ const isPotentiallyChangingReactUseCallback = (initializer, context) => {
45704
+ const unwrappedInitializer = stripParenExpression(initializer);
45705
+ if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return false;
45706
+ if (!isReactApiCall(unwrappedInitializer, "useCallback", context.scopes, {
45707
+ allowGlobalReactNamespace: true,
45708
+ resolveNamedAliases: true
45709
+ })) return false;
45710
+ const dependencyList = unwrappedInitializer.arguments?.[1];
45711
+ if (!dependencyList) return true;
45712
+ const unwrappedDependencyList = stripParenExpression(dependencyList);
45713
+ if (!isNodeOfType(unwrappedDependencyList, "ArrayExpression")) return true;
45714
+ return (unwrappedDependencyList.elements ?? []).some((dependency) => dependency === null || !isStableReactHookDependency(dependency, context));
45715
+ };
45716
+ const collectPotentiallyChangingCallbackBindings = (componentBody, context) => {
45717
+ const potentiallyChangingCallbacks = /* @__PURE__ */ new Set();
45718
+ if (!isNodeOfType(componentBody, "BlockStatement")) return potentiallyChangingCallbacks;
45424
45719
  for (const statement of componentBody.body ?? []) {
45425
45720
  if (!isNodeOfType(statement, "VariableDeclaration")) continue;
45426
45721
  for (const declarator of statement.declarations ?? []) {
45427
45722
  if (!isNodeOfType(declarator.id, "Identifier")) continue;
45428
- if (!isNodeOfType(declarator.init, "CallExpression")) continue;
45429
- if (!isHookCall$2(declarator.init, "useCallback")) continue;
45430
- functionTypedLocals.add(declarator.id.name);
45723
+ if (!declarator.init || !isPotentiallyChangingReactUseCallback(declarator.init, context)) continue;
45724
+ potentiallyChangingCallbacks.add(declarator.id.name);
45431
45725
  }
45432
45726
  }
45433
- return functionTypedLocals;
45727
+ return potentiallyChangingCallbacks;
45434
45728
  };
45435
45729
  const findEnclosingFunctionInsideEffect = (identifierNode, effectCallback) => {
45436
45730
  let cursor = identifierNode.parent ?? null;
@@ -45504,7 +45798,7 @@ const preferUseEffectEvent = defineRule({
45504
45798
  create: (context) => {
45505
45799
  const checkComponent = (componentBody) => {
45506
45800
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
45507
- const functionTypedLocalBindings = collectFunctionTypedLocalBindings(componentBody);
45801
+ const potentiallyChangingCallbackBindings = collectPotentiallyChangingCallbackBindings(componentBody, context);
45508
45802
  for (const statement of componentBody.body ?? []) {
45509
45803
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
45510
45804
  const effectCall = statement.expression;
@@ -45522,7 +45816,7 @@ const preferUseEffectEvent = defineRule({
45522
45816
  if (!isNodeOfType(depElement, "Identifier")) continue;
45523
45817
  const depName = depElement.name;
45524
45818
  const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
45525
- const isFunctionTypedLocalDep = functionTypedLocalBindings.has(depName);
45819
+ const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
45526
45820
  if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
45527
45821
  const classification = classifyCallableReadsInsideEffect(depName, callback);
45528
45822
  if (!classification.hasAnyRead) continue;
@@ -47068,6 +47362,37 @@ const containsLocaleEnvironmentRead = (expression) => {
47068
47362
  return readsLocaleEnvironment;
47069
47363
  };
47070
47364
  //#endregion
47365
+ //#region src/plugin/utils/is-hook-binding-in-scope.ts
47366
+ const isHookBindingInScope = (node, query) => {
47367
+ const { bindingName, hookName, destructureIndex } = query;
47368
+ let cursor = node;
47369
+ while (cursor) {
47370
+ if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
47371
+ if (!isNodeOfType(statement, "VariableDeclaration")) continue;
47372
+ for (const declarator of statement.declarations ?? []) {
47373
+ if (!isNodeOfType(declarator.init, "CallExpression")) continue;
47374
+ if (!isHookCall$2(declarator.init, hookName)) continue;
47375
+ if (destructureIndex !== void 0) {
47376
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
47377
+ const elements = declarator.id.elements ?? [];
47378
+ if (elements.length <= destructureIndex) continue;
47379
+ const element = elements[destructureIndex];
47380
+ if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
47381
+ } else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
47382
+ }
47383
+ }
47384
+ cursor = cursor.parent ?? null;
47385
+ }
47386
+ return false;
47387
+ };
47388
+ //#endregion
47389
+ //#region src/plugin/utils/is-use-state-setter-in-scope.ts
47390
+ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
47391
+ bindingName: setterName,
47392
+ hookName: "useState",
47393
+ destructureIndex: 1
47394
+ });
47395
+ //#endregion
47071
47396
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
47072
47397
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
47073
47398
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -48757,14 +49082,6 @@ const rerenderTransitionsScroll = defineRule({
48757
49082
  } })
48758
49083
  });
48759
49084
  //#endregion
48760
- //#region src/plugin/utils/define-retired-rule.ts
48761
- const defineRetiredRule = (rule) => defineRule({
48762
- ...rule,
48763
- defaultEnabled: false,
48764
- lifecycle: "retired",
48765
- create: () => ({})
48766
- });
48767
- //#endregion
48768
49085
  //#region src/plugin/rules/react-native/rn-animate-layout-property.ts
48769
49086
  const rnAnimateLayoutProperty = defineRetiredRule({
48770
49087
  id: "rn-animate-layout-property",
@@ -57932,8 +58249,7 @@ const webhookSignatureRisk = defineRule({
57932
58249
  });
57933
58250
  //#endregion
57934
58251
  //#region src/plugin/rules/zod/utils/zod-ast.ts
57935
- const ZOD_MODULE = "zod";
57936
- const ZOD_MODULE_SOURCES = [ZOD_MODULE];
58252
+ const ZOD_MODULE_SOURCES = ["zod", "zod/v4"];
57937
58253
  const importInfoCache = /* @__PURE__ */ new WeakMap();
57938
58254
  const getImportInfoForIdentifier = (identifier) => {
57939
58255
  if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
@@ -57946,7 +58262,8 @@ const computeImportInfoForIdentifier = (identifier) => {
57946
58262
  if (!specifier) return null;
57947
58263
  const declaration = specifier.parent;
57948
58264
  if (!declaration || !isNodeOfType(declaration, "ImportDeclaration")) return null;
57949
- if (declaration.source?.value !== ZOD_MODULE) return null;
58265
+ const source = declaration.source?.value;
58266
+ if (typeof source !== "string" || !ZOD_MODULE_SOURCES.includes(source)) return null;
57950
58267
  if (isNodeOfType(specifier, "ImportNamespaceSpecifier")) return {
57951
58268
  imported: null,
57952
58269
  isDefault: false,