oxlint-plugin-react-doctor 0.7.2-dev.fb8ffb0 → 0.7.3-dev.6b70b32

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/dist/index.d.ts +138 -0
  2. package/dist/index.js +2394 -556
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
532
532
  ]);
533
533
  const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
534
534
  const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
535
+ const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
536
+ "WebSocket",
537
+ "EventSource",
538
+ "BroadcastChannel",
539
+ "RTCPeerConnection"
540
+ ]);
535
541
  const MUTABLE_GLOBAL_ROOTS = new Set([
536
542
  "location",
537
543
  "window",
@@ -687,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
687
693
  "unwatch",
688
694
  "unlisten",
689
695
  "unsub",
690
- "abort"
696
+ "abort",
697
+ "disconnect",
698
+ "unobserve",
699
+ "close"
691
700
  ]);
692
701
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
693
702
  "remove",
@@ -782,7 +791,7 @@ const findProgramRoot = (node) => {
782
791
  };
783
792
  //#endregion
784
793
  //#region src/plugin/utils/get-imported-name.ts
785
- const getImportedName$1 = (importSpecifier) => {
794
+ const getImportedName = (importSpecifier) => {
786
795
  if (!isNodeOfType(importSpecifier, "ImportSpecifier")) return void 0;
787
796
  const imported = importSpecifier.imported;
788
797
  if (isNodeOfType(imported, "Identifier")) return imported.name;
@@ -904,7 +913,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
904
913
  continue;
905
914
  }
906
915
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
907
- const importedName = getImportedName$1(specifier);
916
+ const importedName = getImportedName(specifier);
908
917
  if (!importedName || !ACTIVITY_IMPORTED_NAMES.has(importedName)) continue;
909
918
  if (isNodeOfType(specifier.local, "Identifier")) localActivityNames.add(specifier.local.name);
910
919
  }
@@ -2248,6 +2257,39 @@ const anchorHasContent = defineRule({
2248
2257
  }
2249
2258
  });
2250
2259
  //#endregion
2260
+ //#region src/plugin/utils/get-jsx-prop-static-string-values.ts
2261
+ const MAX_CONST_RESOLUTION_HOPS = 4;
2262
+ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2263
+ const expression = stripParenExpression(rawExpression);
2264
+ if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
2265
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2266
+ const staticValue = getStaticTemplateLiteralValue(expression);
2267
+ return staticValue === null ? null : [staticValue];
2268
+ }
2269
+ if (isNodeOfType(expression, "ConditionalExpression")) {
2270
+ const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
2271
+ if (consequentValues === null) return null;
2272
+ const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
2273
+ if (alternateValues === null) return null;
2274
+ return [...consequentValues, ...alternateValues];
2275
+ }
2276
+ if (isNodeOfType(expression, "Identifier")) {
2277
+ if (remainingHops === 0) return null;
2278
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2279
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
2281
+ return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2282
+ }
2283
+ return null;
2284
+ };
2285
+ const getJsxPropStaticStringValues = (attribute, scopes) => {
2286
+ const value = attribute.value;
2287
+ if (!value) return null;
2288
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
2289
+ if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
2290
+ return null;
2291
+ };
2292
+ //#endregion
2251
2293
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2252
2294
  const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
2253
2295
  const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
@@ -2275,30 +2317,14 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2275
2317
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2276
2318
  const isInvalidHref = (value, validHrefs) => {
2277
2319
  if (validHrefs.has(value)) return false;
2278
- return value === "" || value === "#" || value === "javascript:void(0)";
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2279
2322
  };
2280
- const getStaticHrefValue = (value) => {
2281
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
2282
- if (isNodeOfType(value, "JSXExpressionContainer")) {
2283
- const expression = value.expression;
2284
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
2285
- if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
2286
- }
2287
- return null;
2288
- };
2289
- const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
2290
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
2323
+ const isNullishOrFragmentHref = (value) => {
2291
2324
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2292
2325
  const expression = value.expression;
2293
2326
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2294
- if (isNodeOfType(expression, "Literal")) {
2295
- if (expression.value === null) return true;
2296
- if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2297
- }
2298
- if (isNodeOfType(expression, "TemplateLiteral")) {
2299
- const staticValue = getStaticTemplateLiteralValue(expression);
2300
- return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2301
- }
2327
+ if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2302
2328
  }
2303
2329
  if (isNodeOfType(value, "JSXFragment")) return true;
2304
2330
  return false;
@@ -2331,9 +2357,10 @@ const anchorIsValid = defineRule({
2331
2357
  });
2332
2358
  return;
2333
2359
  }
2334
- if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2360
+ const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2361
+ if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2335
2362
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2336
- if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2363
+ if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2337
2364
  context.report({
2338
2365
  node: node.name,
2339
2366
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3389,6 +3416,25 @@ const ariaRole = defineRule({
3389
3416
  if (!roleAttribute) return;
3390
3417
  const elementType = getElementType(node, context.settings);
3391
3418
  if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
3419
+ const reportFirstInvalidCandidate = (candidates) => {
3420
+ for (const candidate of candidates) {
3421
+ if (candidate.trim().length === 0) {
3422
+ context.report({
3423
+ node: roleAttribute,
3424
+ message: buildBaseMessage("")
3425
+ });
3426
+ return;
3427
+ }
3428
+ const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
3429
+ for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3430
+ context.report({
3431
+ node: roleAttribute,
3432
+ message: buildBaseMessage(` \`${token}\` is not one.`)
3433
+ });
3434
+ return;
3435
+ }
3436
+ }
3437
+ };
3392
3438
  const value = roleAttribute.value;
3393
3439
  if (!value) {
3394
3440
  context.report({
@@ -3405,22 +3451,7 @@ const ariaRole = defineRule({
3405
3451
  });
3406
3452
  return;
3407
3453
  }
3408
- const stringValue = value.value;
3409
- if (stringValue.trim().length === 0) {
3410
- context.report({
3411
- node: roleAttribute,
3412
- message: buildBaseMessage("")
3413
- });
3414
- return;
3415
- }
3416
- const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
3417
- for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3418
- context.report({
3419
- node: roleAttribute,
3420
- message: buildBaseMessage(` \`${token}\` is not one.`)
3421
- });
3422
- return;
3423
- }
3454
+ reportFirstInvalidCandidate([value.value]);
3424
3455
  return;
3425
3456
  }
3426
3457
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3441,6 +3472,11 @@ const ariaRole = defineRule({
3441
3472
  });
3442
3473
  return;
3443
3474
  }
3475
+ const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3476
+ if (resolvedCandidates !== null) {
3477
+ reportFirstInvalidCandidate(resolvedCandidates);
3478
+ return;
3479
+ }
3444
3480
  return;
3445
3481
  }
3446
3482
  context.report({
@@ -5157,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
5157
5193
  const callee = node.callee;
5158
5194
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5159
5195
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5160
- if (!isWebStorageObject(callee.object)) return;
5196
+ if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5161
5197
  const keyArgument = node.arguments?.[0];
5162
5198
  if (!keyArgument) return;
5163
5199
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6097,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
6097
6133
  })
6098
6134
  });
6099
6135
  //#endregion
6136
+ //#region src/plugin/utils/is-global-method-call.ts
6137
+ const isGlobalMethodCall = (node, objectName, methodName) => {
6138
+ if (!isNodeOfType(node, "CallExpression")) return false;
6139
+ const callee = stripParenExpression(node.callee);
6140
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
6141
+ const receiver = stripParenExpression(callee.object);
6142
+ return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6143
+ };
6144
+ //#endregion
6100
6145
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6101
6146
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6102
6147
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6103
6148
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6104
6149
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6105
- const isJsonStringifyCall = (node) => {
6106
- if (!isNodeOfType(node, "CallExpression")) return false;
6107
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6108
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6109
- if (node.callee.object.name !== "JSON") return false;
6110
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6111
- return node.callee.property.name === "stringify";
6112
- };
6150
+ const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6113
6151
  const resolveStringKey = (keyArg, context) => {
6114
6152
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6115
6153
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6128,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
6128
6166
  recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
6129
6167
  create: (context) => ({ CallExpression(node) {
6130
6168
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6131
- if (!isNodeOfType(node.callee.object, "Identifier")) return;
6132
- if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
6169
+ const receiver = stripParenExpression(node.callee.object);
6170
+ if (!isNodeOfType(receiver, "Identifier")) return;
6171
+ if (!STORAGE_OBJECTS.has(receiver.name)) return;
6133
6172
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6134
6173
  if (node.callee.property.name !== "setItem") return;
6135
6174
  const keyArg = node.arguments?.[0];
@@ -6142,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
6142
6181
  if (!isJsonStringifyCall(valueArg)) return;
6143
6182
  context.report({
6144
6183
  node: keyArg,
6145
- message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6184
+ message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6146
6185
  });
6147
6186
  } })
6148
6187
  });
@@ -7623,6 +7662,840 @@ const displayName = defineRule({
7623
7662
  }
7624
7663
  });
7625
7664
  //#endregion
7665
+ //#region src/plugin/utils/get-static-property-key-name.ts
7666
+ const getStaticPropertyKeyName = (node, options = {}) => {
7667
+ if (!isNodeOfType(node, "Property")) return null;
7668
+ if (node.computed) {
7669
+ if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
7670
+ return null;
7671
+ }
7672
+ if (isNodeOfType(node.key, "Identifier")) return node.key.name;
7673
+ if (isNodeOfType(node.key, "Literal")) {
7674
+ if (typeof node.key.value === "string") return node.key.value;
7675
+ if (options.stringifyNonStringLiterals) return String(node.key.value);
7676
+ }
7677
+ return null;
7678
+ };
7679
+ //#endregion
7680
+ //#region src/plugin/utils/read-static-boolean.ts
7681
+ const readStaticBoolean = (node) => {
7682
+ if (!node) return null;
7683
+ const unwrappedNode = stripParenExpression(node);
7684
+ if (!isNodeOfType(unwrappedNode, "Literal") || typeof unwrappedNode.value !== "boolean") return null;
7685
+ return unwrappedNode.value;
7686
+ };
7687
+ //#endregion
7688
+ //#region src/plugin/utils/is-react-api-call.ts
7689
+ const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7690
+ const isImportedFromReact = (symbol) => {
7691
+ if (symbol.kind !== "import") return false;
7692
+ const importDeclaration = symbol.declarationNode.parent;
7693
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react");
7694
+ };
7695
+ const isNamedReactApiImport = (identifier, apiNames, scopes) => {
7696
+ if (!isNodeOfType(identifier, "Identifier")) return false;
7697
+ const symbol = scopes.symbolFor(identifier);
7698
+ if (!symbol || !isImportedFromReact(symbol)) return false;
7699
+ const importedName = getImportedName(symbol.declarationNode);
7700
+ return Boolean(importedName && includesApiName(apiNames, importedName));
7701
+ };
7702
+ const isReactNamespaceImport = (identifier, scopes) => {
7703
+ if (!isNodeOfType(identifier, "Identifier")) return false;
7704
+ const symbol = scopes.symbolFor(identifier);
7705
+ if (!symbol || !isImportedFromReact(symbol)) return false;
7706
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
7707
+ };
7708
+ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7709
+ if (!isNodeOfType(node, "CallExpression")) return false;
7710
+ const callee = stripParenExpression(node.callee);
7711
+ if (isNodeOfType(callee, "Identifier")) {
7712
+ if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
7713
+ return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7714
+ }
7715
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
7716
+ if (isReactNamespaceImport(callee.object, scopes)) return true;
7717
+ return Boolean(options.allowGlobalReactNamespace && callee.object.name === "React" && scopes.isGlobalReference(callee.object));
7718
+ };
7719
+ //#endregion
7720
+ //#region src/plugin/rules/state-and-effects/utils/build-listener-cleanup-mismatch-message.ts
7721
+ const buildListenerCleanupMismatchMessage = (eventName, registrationCapture, removalCapture, callbackComparison) => {
7722
+ const hasCallbackMismatch = callbackComparison === "different";
7723
+ if (hasCallbackMismatch && registrationCapture !== removalCapture) return `The cleanup removes \`${eventName}\` with a different callback binding and capture ${String(removalCapture)}, but it was registered with capture ${String(registrationCapture)}. Pass the same callback binding and capture flag to both EventTarget calls.`;
7724
+ if (hasCallbackMismatch) return `The cleanup removes \`${eventName}\` with a different callback binding than the one registered, so \`removeEventListener\` cannot detach that listener. Pass the same callback binding to both calls.`;
7725
+ return `The cleanup removes \`${eventName}\` with capture ${String(removalCapture)}, but it was registered with capture ${String(registrationCapture)}. \`removeEventListener\` must use the same capture flag as \`addEventListener\`.`;
7726
+ };
7727
+ //#endregion
7728
+ //#region src/plugin/rules/state-and-effects/utils/callback-may-register-event-listener.ts
7729
+ const callbackMayRegisterEventListener = (callbackNode) => {
7730
+ let foundCall = false;
7731
+ walkAst(callbackNode, (child) => {
7732
+ if (isNodeOfType(child, "CallExpression")) {
7733
+ foundCall = true;
7734
+ return false;
7735
+ }
7736
+ });
7737
+ return foundCall;
7738
+ };
7739
+ //#endregion
7740
+ //#region src/plugin/rules/state-and-effects/utils/compare-listener-callback-identities.ts
7741
+ const compareListenerCallbackIdentities = (registrationIdentity, removalIdentity) => {
7742
+ if (registrationIdentity.node === removalIdentity.node) return "same";
7743
+ if (registrationIdentity.isConcreteFunction && removalIdentity.isConcreteFunction) return "different";
7744
+ return "unknown";
7745
+ };
7746
+ //#endregion
7747
+ //#region src/plugin/rules/state-and-effects/utils/does-listener-analysis-abort-controller.ts
7748
+ const doesListenerAnalysisAbortController = (analysis, controllerSymbolId) => analysis.abortedControllerSymbolIds.has(controllerSymbolId) || analysis.exhaustiveBranches.some(({ alternate, consequent }) => doesListenerAnalysisAbortController(alternate, controllerSymbolId) && doesListenerAnalysisAbortController(consequent, controllerSymbolId));
7749
+ //#endregion
7750
+ //#region src/plugin/rules/state-and-effects/utils/compare-listener-target-keys.ts
7751
+ const compareListenerTargetKeys = (firstTargetKey, secondTargetKey) => {
7752
+ if (firstTargetKey === secondTargetKey) return "same";
7753
+ if (firstTargetKey.startsWith("global:") && secondTargetKey.startsWith("global:")) return "different";
7754
+ if (firstTargetKey.startsWith("fresh:") || secondTargetKey.startsWith("fresh:")) return "different";
7755
+ return "unknown";
7756
+ };
7757
+ //#endregion
7758
+ //#region src/plugin/rules/state-and-effects/utils/does-listener-analysis-cancel-registration.ts
7759
+ const cancellationResult = (analysis, registration) => {
7760
+ if (registration.abortControllerSymbolId !== null && doesListenerAnalysisAbortController(analysis, registration.abortControllerSymbolId)) return "cancelled";
7761
+ let result = analysis.hasUnknownRemovalCall || registration.abortControllerSymbolId !== null && analysis.hasUnknownAbortCall ? "unknown" : "not-cancelled";
7762
+ for (const removal of analysis.removals) {
7763
+ if (removal.eventName !== registration.eventName || removal.capture !== registration.capture || removal.callbackIdentity === null || compareListenerCallbackIdentities(registration.callbackIdentity, removal.callbackIdentity) !== "same") continue;
7764
+ const targetComparison = compareListenerTargetKeys(registration.targetKey, removal.targetKey);
7765
+ if (targetComparison === "same") return "cancelled";
7766
+ if (targetComparison === "unknown") result = "unknown";
7767
+ }
7768
+ for (const { alternate, consequent } of analysis.exhaustiveBranches) {
7769
+ const alternateResult = cancellationResult(alternate, registration);
7770
+ const consequentResult = cancellationResult(consequent, registration);
7771
+ if (alternateResult === "cancelled" && consequentResult === "cancelled") return "cancelled";
7772
+ if (alternateResult !== "not-cancelled" && consequentResult !== "not-cancelled") result = "unknown";
7773
+ }
7774
+ return result;
7775
+ };
7776
+ const doesListenerAnalysisCancelRegistration = (analysis, registration) => cancellationResult(analysis, registration) !== "not-cancelled";
7777
+ //#endregion
7778
+ //#region src/plugin/rules/state-and-effects/utils/static-member-property-name.ts
7779
+ const getStaticMemberPropertyName = (node) => {
7780
+ if (!node) return null;
7781
+ const unwrappedNode = stripParenExpression(node);
7782
+ if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
7783
+ if (!unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier")) return unwrappedNode.property.name;
7784
+ if (unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Literal") && typeof unwrappedNode.property.value === "string") return unwrappedNode.property.value;
7785
+ return null;
7786
+ };
7787
+ //#endregion
7788
+ //#region src/plugin/rules/state-and-effects/utils/is-listener-path-ambiguous.ts
7789
+ const PATH_AMBIGUOUS_ANCESTOR_TYPES = new Set([
7790
+ "CatchClause",
7791
+ "ConditionalExpression",
7792
+ "DoWhileStatement",
7793
+ "ForInStatement",
7794
+ "ForOfStatement",
7795
+ "ForStatement",
7796
+ "IfStatement",
7797
+ "LogicalExpression",
7798
+ "SwitchCase",
7799
+ "SwitchStatement",
7800
+ "TryStatement",
7801
+ "WhileStatement"
7802
+ ]);
7803
+ const isGuaranteedChild = (parentNode, childNode) => {
7804
+ if (isNodeOfType(parentNode, "IfStatement")) {
7805
+ if (parentNode.test === childNode) return true;
7806
+ const staticTestValue = readStaticBoolean(parentNode.test);
7807
+ return parentNode.consequent === childNode && staticTestValue === true || parentNode.alternate === childNode && staticTestValue === false;
7808
+ }
7809
+ if (isNodeOfType(parentNode, "ConditionalExpression")) {
7810
+ if (parentNode.test === childNode) return true;
7811
+ const staticTestValue = readStaticBoolean(parentNode.test);
7812
+ return parentNode.consequent === childNode && staticTestValue === true || parentNode.alternate === childNode && staticTestValue === false;
7813
+ }
7814
+ if (isNodeOfType(parentNode, "LogicalExpression")) {
7815
+ if (parentNode.left === childNode) return true;
7816
+ if (parentNode.right !== childNode) return false;
7817
+ const staticLeftValue = readStaticBoolean(parentNode.left);
7818
+ return parentNode.operator === "&&" && staticLeftValue === true || parentNode.operator === "||" && staticLeftValue === false;
7819
+ }
7820
+ if (isNodeOfType(parentNode, "TryStatement")) return parentNode.finalizer === childNode;
7821
+ if (isNodeOfType(parentNode, "SwitchStatement")) return parentNode.discriminant === childNode;
7822
+ if (isNodeOfType(parentNode, "ForStatement")) return parentNode.init === childNode || parentNode.test === childNode;
7823
+ if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.right === childNode;
7824
+ if (isNodeOfType(parentNode, "WhileStatement")) return parentNode.test === childNode;
7825
+ return false;
7826
+ };
7827
+ const isListenerPathAmbiguous = (node, bodyNode, allowAmbiguousChild) => {
7828
+ let currentNode = node;
7829
+ while (currentNode && currentNode !== bodyNode) {
7830
+ const parentNode = currentNode.parent;
7831
+ if (!parentNode) return true;
7832
+ if (PATH_AMBIGUOUS_ANCESTOR_TYPES.has(parentNode.type) && !isGuaranteedChild(parentNode, currentNode) && !allowAmbiguousChild?.(parentNode, currentNode)) return true;
7833
+ currentNode = parentNode;
7834
+ }
7835
+ return false;
7836
+ };
7837
+ //#endregion
7838
+ //#region src/plugin/rules/state-and-effects/utils/resolve-event-listener-capture.ts
7839
+ const resolveEventListenerCapture = (optionsNode) => {
7840
+ if (!optionsNode) return false;
7841
+ const unwrappedOptions = stripParenExpression(optionsNode);
7842
+ if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? unwrappedOptions.value : null;
7843
+ if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
7844
+ let capture = false;
7845
+ for (const property of unwrappedOptions.properties) {
7846
+ if (!isNodeOfType(property, "Property")) return null;
7847
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
7848
+ if (propertyName === null) return null;
7849
+ if (!property.computed && propertyName === "__proto__") return null;
7850
+ if (propertyName !== "capture") continue;
7851
+ const propertyValue = stripParenExpression(property.value);
7852
+ if (!isNodeOfType(propertyValue, "Literal") || typeof propertyValue.value !== "boolean") return null;
7853
+ capture = propertyValue.value;
7854
+ }
7855
+ return capture;
7856
+ };
7857
+ //#endregion
7858
+ //#region src/plugin/rules/state-and-effects/utils/resolve-static-once-option.ts
7859
+ const resolveStaticOnceOption = (optionsNode) => {
7860
+ if (!optionsNode) return false;
7861
+ const unwrappedOptions = stripParenExpression(optionsNode);
7862
+ if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? false : null;
7863
+ if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
7864
+ let once = false;
7865
+ for (const property of unwrappedOptions.properties) {
7866
+ if (!isNodeOfType(property, "Property")) return null;
7867
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
7868
+ if (propertyName === null || !property.computed && propertyName === "__proto__") return null;
7869
+ if (propertyName !== "once") continue;
7870
+ const propertyValue = stripParenExpression(property.value);
7871
+ if (!isNodeOfType(propertyValue, "Literal") || typeof propertyValue.value !== "boolean") return null;
7872
+ once = propertyValue.value;
7873
+ }
7874
+ return once;
7875
+ };
7876
+ //#endregion
7877
+ //#region src/plugin/rules/state-and-effects/effect-listener-cleanup-mismatch.ts
7878
+ const LISTENER_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
7879
+ const hasOnlyReadReferences = (symbol) => symbol.references.every((reference) => reference.flag === "read");
7880
+ const isStableSymbol = (symbol) => symbol.kind === "const" || symbol.kind === "import" || (symbol.kind === "function" || symbol.kind === "parameter") && hasOnlyReadReferences(symbol);
7881
+ const isPlainConstSymbol = (symbol) => symbol.kind === "const" && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") && symbol.declarationNode.id === symbol.bindingIdentifier;
7882
+ const resolveAliasedSymbol = (identifier, context, visitedSymbolIds) => {
7883
+ if (!isNodeOfType(identifier, "Identifier")) return null;
7884
+ const symbol = context.scopes.symbolFor(identifier);
7885
+ if (!symbol || !isStableSymbol(symbol) || visitedSymbolIds.has(symbol.id)) return null;
7886
+ if (symbol.kind === "const" && !isPlainConstSymbol(symbol)) return null;
7887
+ visitedSymbolIds.add(symbol.id);
7888
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
7889
+ if (symbol.kind === "const" && isNodeOfType(initializer, "Identifier")) {
7890
+ const resolvedAlias = resolveAliasedSymbol(initializer, context, visitedSymbolIds);
7891
+ if (resolvedAlias) return resolvedAlias;
7892
+ }
7893
+ return symbol;
7894
+ };
7895
+ const resolveCallbackIdentity = (callbackNode, context) => {
7896
+ if (!callbackNode) return null;
7897
+ const unwrappedCallback = stripParenExpression(callbackNode);
7898
+ if (isFunctionLike$1(unwrappedCallback)) return {
7899
+ node: unwrappedCallback,
7900
+ isConcreteFunction: true
7901
+ };
7902
+ if (!isNodeOfType(unwrappedCallback, "Identifier")) return null;
7903
+ const symbol = resolveAliasedSymbol(unwrappedCallback, context, /* @__PURE__ */ new Set());
7904
+ if (!symbol) return null;
7905
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
7906
+ if (initializer && isFunctionLike$1(initializer)) return {
7907
+ node: initializer,
7908
+ isConcreteFunction: true
7909
+ };
7910
+ return {
7911
+ node: symbol.bindingIdentifier,
7912
+ isConcreteFunction: false
7913
+ };
7914
+ };
7915
+ const resolveTargetKey = (targetNode, context) => {
7916
+ const unwrappedTarget = stripParenExpression(targetNode);
7917
+ if (isNodeOfType(unwrappedTarget, "Identifier")) {
7918
+ const symbol = resolveAliasedSymbol(unwrappedTarget, context, /* @__PURE__ */ new Set());
7919
+ if (symbol) {
7920
+ if (symbol.kind === "import" || symbol.kind === "parameter" || symbol.kind === "function") return null;
7921
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
7922
+ if (isNodeOfType(initializer, "NewExpression") && isNodeOfType(initializer.callee, "Identifier") && !context.scopes.isGlobalReference(initializer.callee)) return null;
7923
+ if (isNodeOfType(initializer, "NewExpression")) return `fresh:${symbol.id}`;
7924
+ return `symbol:${symbol.id}`;
7925
+ }
7926
+ if (context.scopes.isGlobalReference(unwrappedTarget)) return `global:${unwrappedTarget.name}`;
7927
+ return null;
7928
+ }
7929
+ if (isNodeOfType(unwrappedTarget, "MemberExpression") && !unwrappedTarget.computed && isNodeOfType(unwrappedTarget.property, "Identifier")) {
7930
+ const objectKey = resolveTargetKey(unwrappedTarget.object, context);
7931
+ if (unwrappedTarget.property.name === "document" && (objectKey === "global:window" || objectKey === "global:globalThis")) return "global:document";
7932
+ if (unwrappedTarget.property.name === "window" && (objectKey === "global:window" || objectKey === "global:globalThis")) return "global:window";
7933
+ return objectKey === null ? null : `${objectKey}.${unwrappedTarget.property.name}`;
7934
+ }
7935
+ return null;
7936
+ };
7937
+ const resolveStaticEventName = (eventNode, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
7938
+ if (!eventNode) return null;
7939
+ const unwrappedEvent = stripParenExpression(eventNode);
7940
+ if (isNodeOfType(unwrappedEvent, "Literal") && typeof unwrappedEvent.value === "string") return unwrappedEvent.value;
7941
+ if (isNodeOfType(unwrappedEvent, "TemplateLiteral") && unwrappedEvent.expressions.length === 0) return unwrappedEvent.quasis[0]?.value.cooked ?? "";
7942
+ if (!isNodeOfType(unwrappedEvent, "Identifier")) return null;
7943
+ const symbol = context.scopes.symbolFor(unwrappedEvent);
7944
+ if (!symbol || !isPlainConstSymbol(symbol) || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return null;
7945
+ visitedSymbolIds.add(symbol.id);
7946
+ return resolveStaticEventName(symbol.initializer, context, visitedSymbolIds);
7947
+ };
7948
+ const readStaticEventDispatch = (node, context) => {
7949
+ const targetNode = readDirectMemberReceiver(node.callee, "dispatchEvent", context);
7950
+ const eventArgument = node.arguments?.[0];
7951
+ if (!eventArgument) return null;
7952
+ const eventNode = stripParenExpression(eventArgument);
7953
+ if (!targetNode || !isNodeOfType(eventNode, "NewExpression") || !isNodeOfType(eventNode.callee, "Identifier") || eventNode.callee.name !== "Event" || !context.scopes.isGlobalReference(eventNode.callee)) return null;
7954
+ const target = stripParenExpression(targetNode);
7955
+ if (!isNodeOfType(target, "Identifier")) return null;
7956
+ const targetSymbol = resolveAliasedSymbol(target, context, /* @__PURE__ */ new Set());
7957
+ const targetInitializer = targetSymbol?.initializer ? stripParenExpression(targetSymbol.initializer) : null;
7958
+ if (!isNodeOfType(targetInitializer, "NewExpression") || !isNodeOfType(targetInitializer.callee, "Identifier") || targetInitializer.callee.name !== "EventTarget" || !context.scopes.isGlobalReference(targetInitializer.callee)) return null;
7959
+ const targetKey = resolveTargetKey(targetNode, context);
7960
+ const eventName = resolveStaticEventName(eventNode.arguments?.[0], context);
7961
+ return targetKey !== null && eventName !== null ? {
7962
+ eventName,
7963
+ targetKey
7964
+ } : null;
7965
+ };
7966
+ const resolveLocalAbortControllerSymbolId = (controllerNode, context) => {
7967
+ const unwrappedController = stripParenExpression(controllerNode);
7968
+ if (!isNodeOfType(unwrappedController, "Identifier")) return null;
7969
+ const controllerSymbol = resolveAliasedSymbol(unwrappedController, context, /* @__PURE__ */ new Set());
7970
+ if (!controllerSymbol || controllerSymbol.kind !== "const" || !controllerSymbol.initializer) return null;
7971
+ const initializer = stripParenExpression(controllerSymbol.initializer);
7972
+ if (!isNodeOfType(initializer, "NewExpression") || !isNodeOfType(initializer.callee, "Identifier") || initializer.callee.name !== "AbortController" || !context.scopes.isGlobalReference(initializer.callee)) return null;
7973
+ return controllerSymbol.id;
7974
+ };
7975
+ const readDirectMemberReceiver = (memberNode, memberName, context) => {
7976
+ if (!memberNode) return null;
7977
+ const unwrappedMember = stripParenExpression(memberNode);
7978
+ if (!isNodeOfType(unwrappedMember, "MemberExpression")) return null;
7979
+ if ((getStaticMemberPropertyName(unwrappedMember) ?? (context && unwrappedMember.computed ? resolveStaticEventName(unwrappedMember.property, context) : null)) !== memberName) return null;
7980
+ return unwrappedMember.object;
7981
+ };
7982
+ const isSignalObjectPatternBinding = (patternNode, bindingIdentifier) => {
7983
+ if (!isNodeOfType(patternNode, "ObjectPattern")) return false;
7984
+ return patternNode.properties.some((property) => {
7985
+ if (!isNodeOfType(property, "Property")) return false;
7986
+ if (getStaticPropertyKeyName(property, { allowComputedString: true }) !== "signal") return false;
7987
+ return (isNodeOfType(property.value, "AssignmentPattern") ? property.value.left : property.value) === bindingIdentifier;
7988
+ });
7989
+ };
7990
+ const resolveSignalAbortControllerSymbolId = (signalNode, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
7991
+ const unwrappedSignal = stripParenExpression(signalNode);
7992
+ const directControllerNode = readDirectMemberReceiver(unwrappedSignal, "signal");
7993
+ if (directControllerNode) return resolveLocalAbortControllerSymbolId(directControllerNode, context);
7994
+ if (!isNodeOfType(unwrappedSignal, "Identifier")) return null;
7995
+ const signalSymbol = context.scopes.symbolFor(unwrappedSignal);
7996
+ if (!signalSymbol || signalSymbol.kind !== "const" || !signalSymbol.initializer || visitedSymbolIds.has(signalSymbol.id) || !hasOnlyReadReferences(signalSymbol)) return null;
7997
+ visitedSymbolIds.add(signalSymbol.id);
7998
+ const declarationNode = signalSymbol.declarationNode;
7999
+ if (!isNodeOfType(declarationNode, "VariableDeclarator")) return null;
8000
+ if (isNodeOfType(declarationNode.id, "Identifier") && declarationNode.id === signalSymbol.bindingIdentifier) {
8001
+ const initializer = stripParenExpression(signalSymbol.initializer);
8002
+ if (isNodeOfType(initializer, "Identifier")) return resolveSignalAbortControllerSymbolId(initializer, context, visitedSymbolIds);
8003
+ const controllerNode = readDirectMemberReceiver(initializer, "signal");
8004
+ return controllerNode ? resolveLocalAbortControllerSymbolId(controllerNode, context) : null;
8005
+ }
8006
+ if (!isSignalObjectPatternBinding(declarationNode.id, signalSymbol.bindingIdentifier)) return null;
8007
+ return resolveLocalAbortControllerSymbolId(signalSymbol.initializer, context);
8008
+ };
8009
+ const readControllerAbortedCondition = (conditionNode, controllerSymbolId, context) => {
8010
+ const unwrappedCondition = stripParenExpression(conditionNode);
8011
+ if (isNodeOfType(unwrappedCondition, "UnaryExpression") && unwrappedCondition.operator === "!") {
8012
+ const operandCondition = readControllerAbortedCondition(unwrappedCondition.argument, controllerSymbolId, context);
8013
+ if (operandCondition === "aborted") return "not-aborted";
8014
+ if (operandCondition === "not-aborted") return "aborted";
8015
+ return null;
8016
+ }
8017
+ if (isNodeOfType(unwrappedCondition, "BinaryExpression")) {
8018
+ const leftBoolean = readStaticBoolean(unwrappedCondition.left);
8019
+ const rightBoolean = readStaticBoolean(unwrappedCondition.right);
8020
+ const memberNode = leftBoolean === null ? unwrappedCondition.left : rightBoolean === null ? unwrappedCondition.right : null;
8021
+ const comparedBoolean = leftBoolean ?? rightBoolean;
8022
+ if (memberNode && comparedBoolean !== null) {
8023
+ const memberCondition = readControllerAbortedCondition(memberNode, controllerSymbolId, context);
8024
+ if (memberCondition) {
8025
+ const isEquality = unwrappedCondition.operator === "==" || unwrappedCondition.operator === "===";
8026
+ const isInequality = unwrappedCondition.operator === "!=" || unwrappedCondition.operator === "!==";
8027
+ const shouldInvert = isEquality && !comparedBoolean || isInequality && comparedBoolean;
8028
+ if (isEquality || isInequality) {
8029
+ if (!shouldInvert) return memberCondition;
8030
+ return memberCondition === "aborted" ? "not-aborted" : "aborted";
8031
+ }
8032
+ }
8033
+ }
8034
+ return null;
8035
+ }
8036
+ const signalNode = readDirectMemberReceiver(unwrappedCondition, "aborted");
8037
+ if (!signalNode) return null;
8038
+ return resolveSignalAbortControllerSymbolId(signalNode, context) === controllerSymbolId ? "aborted" : null;
8039
+ };
8040
+ const isAbortGuardForChild = (parentNode, childNode, controllerSymbolId, context) => {
8041
+ if (isNodeOfType(parentNode, "IfStatement") || isNodeOfType(parentNode, "ConditionalExpression")) {
8042
+ const condition = readControllerAbortedCondition(parentNode.test, controllerSymbolId, context);
8043
+ return parentNode.consequent === childNode && condition === "not-aborted" || parentNode.alternate === childNode && condition === "aborted";
8044
+ }
8045
+ if (!isNodeOfType(parentNode, "LogicalExpression") || parentNode.right !== childNode) return false;
8046
+ const condition = readControllerAbortedCondition(parentNode.left, controllerSymbolId, context);
8047
+ return parentNode.operator === "&&" && condition === "not-aborted" || parentNode.operator === "||" && condition === "aborted";
8048
+ };
8049
+ const isAbortGuaranteedByPath = (node, bodyNode, controllerSymbolId, context) => !isListenerPathAmbiguous(node, bodyNode, (parentNode, childNode) => isAbortGuardForChild(parentNode, childNode, controllerSymbolId, context));
8050
+ const resolveBoundAbortControllerSymbolId = (callNode, context) => {
8051
+ const callee = stripParenExpression(callNode.callee);
8052
+ if (!isNodeOfType(callee, "Identifier")) return null;
8053
+ const boundAbortSymbol = resolveAliasedSymbol(callee, context, /* @__PURE__ */ new Set());
8054
+ if (!boundAbortSymbol?.initializer) return null;
8055
+ const initializer = stripParenExpression(boundAbortSymbol.initializer);
8056
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
8057
+ const abortMethodNode = readDirectMemberReceiver(initializer.callee, "bind");
8058
+ if (!abortMethodNode) return null;
8059
+ const controllerNode = readDirectMemberReceiver(abortMethodNode, "abort");
8060
+ const boundThisNode = initializer.arguments?.[0];
8061
+ if (!controllerNode || !boundThisNode) return null;
8062
+ const controllerSymbolId = resolveLocalAbortControllerSymbolId(controllerNode, context);
8063
+ const boundThisSymbolId = resolveLocalAbortControllerSymbolId(boundThisNode, context);
8064
+ return controllerSymbolId !== null && controllerSymbolId === boundThisSymbolId ? controllerSymbolId : null;
8065
+ };
8066
+ const resolveRegistrationCancellation = (optionsNode, context) => {
8067
+ const noCancellation = {
8068
+ abortControllerSymbolId: null,
8069
+ hasUnknownCancellation: false
8070
+ };
8071
+ if (!optionsNode) return noCancellation;
8072
+ const unwrappedOptions = stripParenExpression(optionsNode);
8073
+ if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return noCancellation;
8074
+ let resolvedCancellation = null;
8075
+ for (const property of unwrappedOptions.properties) {
8076
+ if (!isNodeOfType(property, "Property")) return noCancellation;
8077
+ if (getStaticPropertyKeyName(property, { allowComputedString: true }) !== "signal") continue;
8078
+ if (resolvedCancellation) return {
8079
+ abortControllerSymbolId: null,
8080
+ hasUnknownCancellation: true
8081
+ };
8082
+ const abortControllerSymbolId = resolveSignalAbortControllerSymbolId(property.value, context);
8083
+ resolvedCancellation = {
8084
+ abortControllerSymbolId,
8085
+ hasUnknownCancellation: abortControllerSymbolId === null
8086
+ };
8087
+ }
8088
+ return resolvedCancellation ?? noCancellation;
8089
+ };
8090
+ const readListenerCandidate = (node, methodName, context) => {
8091
+ const targetNode = readDirectMemberReceiver(node.callee, methodName, context);
8092
+ if (!targetNode) return null;
8093
+ const targetKey = resolveTargetKey(targetNode, context);
8094
+ const eventName = resolveStaticEventName(node.arguments?.[0], context);
8095
+ const callbackIdentity = resolveCallbackIdentity(node.arguments?.[1], context);
8096
+ const capture = resolveEventListenerCapture(node.arguments?.[2]);
8097
+ if (targetKey === null || eventName === null) return null;
8098
+ return {
8099
+ node,
8100
+ targetKey,
8101
+ eventName,
8102
+ callbackIdentity,
8103
+ capture
8104
+ };
8105
+ };
8106
+ const readDestructuredRemovalCandidate = (node, context) => {
8107
+ const methodNode = readDirectMemberReceiver(node.callee, "call");
8108
+ if (!methodNode || !isNodeOfType(methodNode, "Identifier")) return null;
8109
+ const methodSymbol = context.scopes.symbolFor(methodNode);
8110
+ if (!methodSymbol || methodSymbol.kind !== "const" || !hasOnlyReadReferences(methodSymbol) || !isNodeOfType(methodSymbol.declarationNode, "VariableDeclarator")) return null;
8111
+ const declaration = methodSymbol.declarationNode;
8112
+ if (!isNodeOfType(declaration.id, "ObjectPattern") || !declaration.init) return null;
8113
+ if (!declaration.id.properties.some((property) => {
8114
+ if (!isNodeOfType(property, "Property")) return false;
8115
+ const bindingNode = isNodeOfType(property.value, "AssignmentPattern") ? property.value.left : property.value;
8116
+ return getStaticPropertyKeyName(property, { allowComputedString: true }) === "removeEventListener" && bindingNode === methodSymbol.bindingIdentifier;
8117
+ })) return null;
8118
+ const targetNode = node.arguments?.[0];
8119
+ if (!targetNode) return null;
8120
+ const declarationTargetKey = resolveTargetKey(declaration.init, context);
8121
+ const targetKey = resolveTargetKey(targetNode, context);
8122
+ if (declarationTargetKey === null || targetKey === null || declarationTargetKey !== targetKey) return null;
8123
+ const eventName = resolveStaticEventName(node.arguments?.[1], context);
8124
+ if (eventName === null) return null;
8125
+ return {
8126
+ node,
8127
+ targetKey,
8128
+ eventName,
8129
+ callbackIdentity: resolveCallbackIdentity(node.arguments?.[2], context),
8130
+ capture: resolveEventListenerCapture(node.arguments?.[3])
8131
+ };
8132
+ };
8133
+ const resolveReturnedCleanupBody = (returnedValue, context) => {
8134
+ if (!returnedValue) return null;
8135
+ const unwrappedValue = stripParenExpression(returnedValue);
8136
+ if (isFunctionLike$1(unwrappedValue)) return unwrappedValue.body;
8137
+ if (!isNodeOfType(unwrappedValue, "Identifier")) return null;
8138
+ const symbol = resolveAliasedSymbol(unwrappedValue, context, /* @__PURE__ */ new Set());
8139
+ if (!symbol || !symbol.initializer) return null;
8140
+ const initializer = stripParenExpression(symbol.initializer);
8141
+ return isFunctionLike$1(initializer) ? initializer.body : null;
8142
+ };
8143
+ const collectEffectListenerInputs = (effectBody, context) => {
8144
+ const registrations = [];
8145
+ const cleanupBodies = [];
8146
+ const listenerRegistrationCountsByTarget = /* @__PURE__ */ new Map();
8147
+ let returnStatementCount = 0;
8148
+ if (!isNodeOfType(effectBody, "BlockStatement")) return {
8149
+ registrations,
8150
+ cleanupBodies,
8151
+ hasCanonicalCleanupReturn: false,
8152
+ returnStatementCount
8153
+ };
8154
+ const finalEffectStatement = effectBody.body[effectBody.body.length - 1];
8155
+ const hasCanonicalCleanupReturn = isNodeOfType(finalEffectStatement, "ReturnStatement");
8156
+ walkAst(effectBody, (child) => {
8157
+ if (child !== effectBody && isFunctionLike$1(child)) return false;
8158
+ if (isNodeOfType(child, "ClassDeclaration") || isNodeOfType(child, "ClassExpression")) return false;
8159
+ if (isNodeOfType(child, "CallExpression")) {
8160
+ if (isListenerPathAmbiguous(child, effectBody)) return;
8161
+ const registrationTarget = readDirectMemberReceiver(child.callee, "addEventListener", context);
8162
+ const registrationTargetKey = registrationTarget ? resolveTargetKey(registrationTarget, context) : null;
8163
+ if (registrationTargetKey !== null) listenerRegistrationCountsByTarget.set(registrationTargetKey, (listenerRegistrationCountsByTarget.get(registrationTargetKey) ?? 0) + 1);
8164
+ const candidate = readListenerCandidate(child, "addEventListener", context);
8165
+ if (candidate?.callbackIdentity && candidate.capture !== null) {
8166
+ const registrationCancellation = resolveRegistrationCancellation(candidate.node.arguments?.[2], context);
8167
+ registrations.push({
8168
+ node: candidate.node,
8169
+ targetKey: candidate.targetKey,
8170
+ eventName: candidate.eventName,
8171
+ callbackIdentity: candidate.callbackIdentity,
8172
+ capture: candidate.capture,
8173
+ once: resolveStaticOnceOption(candidate.node.arguments?.[2]) === true,
8174
+ abortControllerSymbolId: registrationCancellation.abortControllerSymbolId,
8175
+ hasUnknownCancellation: registrationCancellation.hasUnknownCancellation
8176
+ });
8177
+ }
8178
+ const eventDispatch = readStaticEventDispatch(child, context);
8179
+ if (eventDispatch) {
8180
+ const dispatchedRegistrations = registrations.filter((registration) => registration.targetKey === eventDispatch.targetKey && registration.eventName === eventDispatch.eventName);
8181
+ const dispatchedRegistration = dispatchedRegistrations[0];
8182
+ if (dispatchedRegistrations.length === 1 && dispatchedRegistration?.once && listenerRegistrationCountsByTarget.get(eventDispatch.targetKey) === 1 && dispatchedRegistration.callbackIdentity.isConcreteFunction && !callbackMayRegisterEventListener(dispatchedRegistration.callbackIdentity.node)) registrations.splice(registrations.indexOf(dispatchedRegistration), 1);
8183
+ }
8184
+ return;
8185
+ }
8186
+ if (isNodeOfType(child, "ReturnStatement")) {
8187
+ returnStatementCount += 1;
8188
+ const cleanupBody = resolveReturnedCleanupBody(child.argument, context);
8189
+ if (cleanupBody) cleanupBodies.push(cleanupBody);
8190
+ }
8191
+ });
8192
+ return {
8193
+ registrations,
8194
+ cleanupBodies,
8195
+ hasCanonicalCleanupReturn,
8196
+ returnStatementCount
8197
+ };
8198
+ };
8199
+ const resolveCalledCleanup = (callNode, context) => {
8200
+ const callee = stripParenExpression(callNode.callee);
8201
+ if (!isNodeOfType(callee, "Identifier")) return null;
8202
+ const cleanupSymbol = resolveAliasedSymbol(callee, context, /* @__PURE__ */ new Set());
8203
+ if (!cleanupSymbol?.initializer || cleanupSymbol.kind !== "const" && cleanupSymbol.kind !== "function") return null;
8204
+ const initializer = stripParenExpression(cleanupSymbol.initializer);
8205
+ if (!isFunctionLike$1(initializer) || initializer.async || initializer.generator) return null;
8206
+ return {
8207
+ body: initializer.body,
8208
+ symbolId: cleanupSymbol.id
8209
+ };
8210
+ };
8211
+ const analyzeCleanupBody = (cleanupBody, context, visitedCleanupSymbolIds = /* @__PURE__ */ new Set(), isLoopBody = false) => {
8212
+ const removals = [];
8213
+ const abortedControllerSymbolIds = /* @__PURE__ */ new Set();
8214
+ const exhaustiveBranches = [];
8215
+ let hasAmbiguousReachability = false;
8216
+ let hasUnknownAbortCall = false;
8217
+ let hasUnknownRemovalCall = false;
8218
+ const finalCleanupStatement = isNodeOfType(cleanupBody, "BlockStatement") ? cleanupBody.body[cleanupBody.body.length - 1] : null;
8219
+ const addCleanupAnalysis = (analysis) => {
8220
+ removals.push(...analysis.removals);
8221
+ hasUnknownAbortCall ||= analysis.hasUnknownAbortCall;
8222
+ hasUnknownRemovalCall ||= analysis.hasUnknownRemovalCall;
8223
+ for (const controllerSymbolId of analysis.abortedControllerSymbolIds) abortedControllerSymbolIds.add(controllerSymbolId);
8224
+ exhaustiveBranches.push(...analysis.exhaustiveBranches);
8225
+ };
8226
+ const addGuaranteedLoopPrefix = (loopBody) => {
8227
+ const loopStatements = isNodeOfType(loopBody, "BlockStatement") ? loopBody.body : [loopBody];
8228
+ for (const loopStatement of loopStatements) {
8229
+ if (isNodeOfType(loopStatement, "BreakStatement") || isNodeOfType(loopStatement, "ContinueStatement")) return true;
8230
+ if (isNodeOfType(loopStatement, "BlockStatement")) {
8231
+ if (addGuaranteedLoopPrefix(loopStatement)) return true;
8232
+ continue;
8233
+ }
8234
+ if (isNodeOfType(loopStatement, "IfStatement")) {
8235
+ const staticTestValue = readStaticBoolean(loopStatement.test);
8236
+ if (staticTestValue !== null) {
8237
+ const testAnalysis = analyzeCleanupBody(loopStatement.test, context, new Set(visitedCleanupSymbolIds), true);
8238
+ if (!testAnalysis) return true;
8239
+ addCleanupAnalysis(testAnalysis);
8240
+ const guaranteedBranch = staticTestValue ? loopStatement.consequent : loopStatement.alternate;
8241
+ if (guaranteedBranch && addGuaranteedLoopPrefix(guaranteedBranch)) return true;
8242
+ continue;
8243
+ }
8244
+ }
8245
+ const loopStatementAnalysis = analyzeCleanupBody(loopStatement, context, new Set(visitedCleanupSymbolIds), true);
8246
+ if (!loopStatementAnalysis) return true;
8247
+ addCleanupAnalysis(loopStatementAnalysis);
8248
+ }
8249
+ return false;
8250
+ };
8251
+ walkAst(cleanupBody, (child) => {
8252
+ if (child !== cleanupBody && isFunctionLike$1(child)) return false;
8253
+ if (isNodeOfType(child, "ClassDeclaration") || isNodeOfType(child, "ClassExpression")) return false;
8254
+ if (isNodeOfType(child, "ReturnStatement") && child === finalCleanupStatement) return;
8255
+ if (isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement") || isLoopBody && (isNodeOfType(child, "BreakStatement") || isNodeOfType(child, "ContinueStatement"))) {
8256
+ hasAmbiguousReachability = true;
8257
+ return false;
8258
+ }
8259
+ if (isNodeOfType(child, "ForOfStatement")) {
8260
+ const iterable = stripParenExpression(child.right);
8261
+ if (isNodeOfType(iterable, "ArrayExpression") && iterable.elements.some((element) => element && !isNodeOfType(element, "SpreadElement")) && !isListenerPathAmbiguous(child, cleanupBody)) addGuaranteedLoopPrefix(child.body);
8262
+ return false;
8263
+ }
8264
+ if ((isNodeOfType(child, "IfStatement") || isNodeOfType(child, "ConditionalExpression")) && child.alternate && readStaticBoolean(child.test) === null && !isListenerPathAmbiguous(child, cleanupBody)) {
8265
+ const consequentAnalysis = analyzeCleanupBody(child.consequent, context, new Set(visitedCleanupSymbolIds));
8266
+ const alternateAnalysis = analyzeCleanupBody(child.alternate, context, new Set(visitedCleanupSymbolIds));
8267
+ if (consequentAnalysis && alternateAnalysis) {
8268
+ exhaustiveBranches.push({
8269
+ alternate: alternateAnalysis,
8270
+ consequent: consequentAnalysis
8271
+ });
8272
+ hasUnknownAbortCall ||= consequentAnalysis.hasUnknownAbortCall && alternateAnalysis.hasUnknownAbortCall;
8273
+ hasUnknownRemovalCall ||= consequentAnalysis.hasUnknownRemovalCall && alternateAnalysis.hasUnknownRemovalCall;
8274
+ }
8275
+ const testAnalysis = analyzeCleanupBody(child.test, context, new Set(visitedCleanupSymbolIds));
8276
+ if (testAnalysis) addCleanupAnalysis(testAnalysis);
8277
+ return false;
8278
+ }
8279
+ if (!isNodeOfType(child, "CallExpression")) return;
8280
+ const hasAmbiguousPath = isListenerPathAmbiguous(child, cleanupBody);
8281
+ const removalTarget = readDirectMemberReceiver(child.callee, "removeEventListener", context);
8282
+ if (removalTarget && !hasAmbiguousPath) {
8283
+ const removal = readListenerCandidate(child, "removeEventListener", context);
8284
+ if (removal) {
8285
+ removals.push(removal);
8286
+ hasUnknownRemovalCall ||= removal.callbackIdentity === null || removal.capture === null;
8287
+ } else hasUnknownRemovalCall = true;
8288
+ }
8289
+ if (!removalTarget && !hasAmbiguousPath) {
8290
+ const destructuredRemoval = readDestructuredRemovalCandidate(child, context);
8291
+ if (destructuredRemoval) removals.push(destructuredRemoval);
8292
+ }
8293
+ const controllerNode = readDirectMemberReceiver(child.callee, "abort");
8294
+ if (controllerNode) {
8295
+ const controllerSymbolId = resolveLocalAbortControllerSymbolId(controllerNode, context);
8296
+ if (controllerSymbolId === null) {
8297
+ if (!hasAmbiguousPath) hasUnknownAbortCall = true;
8298
+ } else if (!hasAmbiguousPath || isAbortGuaranteedByPath(child, cleanupBody, controllerSymbolId, context)) abortedControllerSymbolIds.add(controllerSymbolId);
8299
+ return;
8300
+ }
8301
+ if (hasAmbiguousPath) return;
8302
+ const boundAbortControllerSymbolId = resolveBoundAbortControllerSymbolId(child, context);
8303
+ if (boundAbortControllerSymbolId !== null) {
8304
+ abortedControllerSymbolIds.add(boundAbortControllerSymbolId);
8305
+ return;
8306
+ }
8307
+ const calledCleanup = resolveCalledCleanup(child, context);
8308
+ if (!calledCleanup || visitedCleanupSymbolIds.has(calledCleanup.symbolId)) return;
8309
+ visitedCleanupSymbolIds.add(calledCleanup.symbolId);
8310
+ const calledCleanupAnalysis = analyzeCleanupBody(calledCleanup.body, context, visitedCleanupSymbolIds);
8311
+ if (!calledCleanupAnalysis) return;
8312
+ addCleanupAnalysis(calledCleanupAnalysis);
8313
+ });
8314
+ if (hasAmbiguousReachability) return null;
8315
+ return {
8316
+ removals,
8317
+ abortedControllerSymbolIds,
8318
+ exhaustiveBranches,
8319
+ hasUnknownAbortCall,
8320
+ hasUnknownRemovalCall
8321
+ };
8322
+ };
8323
+ const analyzeEffectListeners = (effectBody, context) => {
8324
+ const effectInputs = collectEffectListenerInputs(effectBody, context);
8325
+ if (!effectInputs.hasCanonicalCleanupReturn || effectInputs.returnStatementCount !== 1 || effectInputs.cleanupBodies.length !== 1) return null;
8326
+ const removals = [];
8327
+ const abortedControllerSymbolIds = /* @__PURE__ */ new Set();
8328
+ const exhaustiveBranches = [];
8329
+ let hasUnknownAbortCall = false;
8330
+ let hasUnknownRemovalCall = false;
8331
+ const addAnalysis = (analysis) => {
8332
+ removals.push(...analysis.removals);
8333
+ exhaustiveBranches.push(...analysis.exhaustiveBranches);
8334
+ hasUnknownAbortCall ||= analysis.hasUnknownAbortCall;
8335
+ hasUnknownRemovalCall ||= analysis.hasUnknownRemovalCall;
8336
+ for (const controllerSymbolId of analysis.abortedControllerSymbolIds) abortedControllerSymbolIds.add(controllerSymbolId);
8337
+ };
8338
+ const setupAbortAnalysis = analyzeCleanupBody(effectBody, context);
8339
+ for (const cleanupBody of effectInputs.cleanupBodies) {
8340
+ const cleanupAnalysis = analyzeCleanupBody(cleanupBody, context);
8341
+ if (!cleanupAnalysis) return null;
8342
+ addAnalysis(cleanupAnalysis);
8343
+ }
8344
+ return {
8345
+ registrations: effectInputs.registrations,
8346
+ removals,
8347
+ abortedControllerSymbolIds,
8348
+ exhaustiveBranches,
8349
+ hasUnknownAbortCall,
8350
+ hasUnknownRemovalCall,
8351
+ setupAbortAnalysis
8352
+ };
8353
+ };
8354
+ const effectListenerCleanupMismatch = defineRule({
8355
+ id: "effect-listener-cleanup-mismatch",
8356
+ title: "Effect cleanup does not match its event listener",
8357
+ severity: "error",
8358
+ recommendation: "Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",
8359
+ create: (context) => ({ CallExpression(node) {
8360
+ if (!isReactApiCall(node, LISTENER_EFFECT_HOOK_NAMES, context.scopes)) return;
8361
+ const effectCallback = getEffectCallback(node);
8362
+ if (!isFunctionLike$1(effectCallback)) return;
8363
+ const listenerAnalysis = analyzeEffectListeners(effectCallback.body, context);
8364
+ if (!listenerAnalysis) return;
8365
+ for (const registration of listenerAnalysis.registrations) {
8366
+ if (registration.hasUnknownCancellation || listenerAnalysis.hasUnknownRemovalCall) continue;
8367
+ if (listenerAnalysis.registrations.filter((candidateRegistration) => candidateRegistration.targetKey === registration.targetKey && candidateRegistration.eventName === registration.eventName).length > 1) continue;
8368
+ if (registration.abortControllerSymbolId !== null && listenerAnalysis.hasUnknownAbortCall) continue;
8369
+ if (registration.abortControllerSymbolId !== null && listenerAnalysis.setupAbortAnalysis && doesListenerAnalysisAbortController(listenerAnalysis.setupAbortAnalysis, registration.abortControllerSymbolId)) continue;
8370
+ if (doesListenerAnalysisCancelRegistration(listenerAnalysis, registration)) continue;
8371
+ const candidateRemovals = listenerAnalysis.removals.filter((removal) => removal.targetKey === registration.targetKey && removal.eventName === registration.eventName);
8372
+ let firstProvableMismatch;
8373
+ let didFindNonMismatchCandidate = false;
8374
+ for (const removal of candidateRemovals) {
8375
+ if (!removal.callbackIdentity || removal.capture === null) {
8376
+ didFindNonMismatchCandidate = true;
8377
+ break;
8378
+ }
8379
+ const callbackComparison = compareListenerCallbackIdentities(registration.callbackIdentity, removal.callbackIdentity);
8380
+ if (callbackComparison === "unknown") {
8381
+ didFindNonMismatchCandidate = true;
8382
+ break;
8383
+ }
8384
+ if (callbackComparison === "same" && registration.capture === removal.capture) {
8385
+ didFindNonMismatchCandidate = true;
8386
+ break;
8387
+ }
8388
+ firstProvableMismatch ??= {
8389
+ removalNode: removal.node,
8390
+ removalCapture: removal.capture,
8391
+ callbackComparison
8392
+ };
8393
+ }
8394
+ if (candidateRemovals.length === 0 || didFindNonMismatchCandidate || !firstProvableMismatch) continue;
8395
+ context.report({
8396
+ node: firstProvableMismatch.removalNode,
8397
+ message: buildListenerCleanupMismatchMessage(registration.eventName, registration.capture, firstProvableMismatch.removalCapture, firstProvableMismatch.callbackComparison)
8398
+ });
8399
+ }
8400
+ } })
8401
+ });
8402
+ //#endregion
8403
+ //#region src/plugin/utils/is-react-hook-name.ts
8404
+ const isReactHookName = (name) => {
8405
+ if (!name.startsWith("use")) return false;
8406
+ if (name.length === 3) return true;
8407
+ const fourthCharacter = name.charCodeAt(3);
8408
+ return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8409
+ };
8410
+ //#endregion
8411
+ //#region src/plugin/utils/is-react-component-or-hook-name.ts
8412
+ const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8413
+ //#endregion
8414
+ //#region src/plugin/utils/component-or-hook-display-name.ts
8415
+ const hocWrapperCalleeName = (callee) => {
8416
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8417
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8418
+ return null;
8419
+ };
8420
+ const displayNameFromFunctionBinding = (functionNode) => {
8421
+ let current = functionNode;
8422
+ for (;;) {
8423
+ const parent = current.parent;
8424
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
8425
+ const calleeName = hocWrapperCalleeName(parent.callee);
8426
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
8427
+ current = parent;
8428
+ continue;
8429
+ }
8430
+ }
8431
+ break;
8432
+ }
8433
+ const binding = current.parent;
8434
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
8435
+ return null;
8436
+ };
8437
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
8438
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
8439
+ return displayNameFromFunctionBinding(functionNode);
8440
+ };
8441
+ //#endregion
8442
+ //#region src/plugin/utils/find-enclosing-function.ts
8443
+ const findEnclosingFunction = (node) => {
8444
+ let cursor = node.parent;
8445
+ while (cursor) {
8446
+ if (isFunctionLike$1(cursor)) return cursor;
8447
+ cursor = cursor.parent ?? null;
8448
+ }
8449
+ return null;
8450
+ };
8451
+ //#endregion
8452
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
8453
+ const enclosingComponentOrHookName = (node) => {
8454
+ const functionNode = findEnclosingFunction(node);
8455
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
8456
+ };
8457
+ //#endregion
8458
+ //#region src/plugin/utils/get-range-start.ts
8459
+ const getRangeStart = (node) => {
8460
+ const rangeStart = node.range?.[0];
8461
+ return typeof rangeStart === "number" ? rangeStart : null;
8462
+ };
8463
+ //#endregion
8464
+ //#region src/plugin/utils/is-result-discarded-call.ts
8465
+ const isResultDiscardedCall = (callExpression) => {
8466
+ let node = callExpression;
8467
+ let parent = node.parent;
8468
+ while (parent) {
8469
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
8470
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
8471
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
8472
+ if (isNodeOfType(parent, "ChainExpression")) {
8473
+ node = parent;
8474
+ parent = node.parent;
8475
+ continue;
8476
+ }
8477
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
8478
+ node = parent;
8479
+ parent = node.parent;
8480
+ continue;
8481
+ }
8482
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
8483
+ node = parent;
8484
+ parent = node.parent;
8485
+ continue;
8486
+ }
8487
+ if (isNodeOfType(parent, "SequenceExpression")) {
8488
+ const expressions = parent.expressions ?? [];
8489
+ if (expressions[expressions.length - 1] !== node) return true;
8490
+ node = parent;
8491
+ parent = node.parent;
8492
+ continue;
8493
+ }
8494
+ return false;
8495
+ }
8496
+ return false;
8497
+ };
8498
+ //#endregion
7626
8499
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7627
8500
  const walkInsideStatementBlocks = (node, visitor) => {
7628
8501
  if (!node || typeof node !== "object") return;
@@ -7774,6 +8647,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7774
8647
  };
7775
8648
  //#endregion
7776
8649
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
8650
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
8651
+ const RESOURCE_NOUN_BY_KIND = {
8652
+ subscribe: "subscription",
8653
+ timer: "timer",
8654
+ socket: "connection"
8655
+ };
8656
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
8657
+ const isSubscribeOrObserveCall = (node) => {
8658
+ if (isSubscribeLikeCallExpression(node)) return true;
8659
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
8660
+ };
7777
8661
  const findSubscribeLikeUsages = (callback) => {
7778
8662
  const usages = [];
7779
8663
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7785,6 +8669,14 @@ const findSubscribeLikeUsages = (callback) => {
7785
8669
  }
7786
8670
  walkAst(callback, (child) => {
7787
8671
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
8672
+ if (isSocketConstruction(child)) {
8673
+ usages.push({
8674
+ kind: "socket",
8675
+ node: child,
8676
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8677
+ });
8678
+ return;
8679
+ }
7788
8680
  if (!isNodeOfType(child, "CallExpression")) return;
7789
8681
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7790
8682
  usages.push({
@@ -7794,7 +8686,7 @@ const findSubscribeLikeUsages = (callback) => {
7794
8686
  });
7795
8687
  return;
7796
8688
  }
7797
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
8689
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
7798
8690
  kind: "subscribe",
7799
8691
  node: child,
7800
8692
  resourceName: child.callee.property.name
@@ -7810,6 +8702,13 @@ const collectCleanupBindings = (effectCallback) => {
7810
8702
  };
7811
8703
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7812
8704
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
8705
+ walkAst(effectCallback.body, (child) => {
8706
+ if (!isSubscribeOrObserveCall(child)) return;
8707
+ if (!isNodeOfType(child, "CallExpression")) return;
8708
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
8709
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
8710
+ bindings.subscriptionNames.add(child.callee.object.name);
8711
+ });
7813
8712
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7814
8713
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7815
8714
  for (const declarator of child.declarations ?? []) {
@@ -7817,7 +8716,12 @@ const collectCleanupBindings = (effectCallback) => {
7817
8716
  const bindingName = declarator.id.name;
7818
8717
  bindings.effectScopeVariableNames.add(bindingName);
7819
8718
  const init = declarator.init;
7820
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
8719
+ if (!init) continue;
8720
+ if (isSocketConstruction(init)) {
8721
+ bindings.subscriptionNames.add(bindingName);
8722
+ continue;
8723
+ }
8724
+ if (!isNodeOfType(init, "CallExpression")) continue;
7821
8725
  if (isSubscribeLikeCallExpression(init)) {
7822
8726
  bindings.subscriptionNames.add(bindingName);
7823
8727
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7843,9 +8747,21 @@ const collectCleanupBindings = (effectCallback) => {
7843
8747
  });
7844
8748
  return bindings;
7845
8749
  };
7846
- const getRangeStart = (node) => {
7847
- const rangeStart = node.range?.[0];
7848
- return typeof rangeStart === "number" ? rangeStart : null;
8750
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8751
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8752
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8753
+ const releaseStarts = [];
8754
+ walkInsideStatementBlocks(callback.body, (child) => {
8755
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8756
+ const releaseStart = getRangeStart(child);
8757
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8758
+ });
8759
+ if (releaseStarts.length === 0) return usages;
8760
+ return usages.filter((usage) => {
8761
+ const usageStart = getRangeStart(usage.node);
8762
+ if (usageStart === null) return true;
8763
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8764
+ });
7849
8765
  };
7850
8766
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7851
8767
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
@@ -7869,26 +8785,177 @@ const effectHasCleanupReturn = (callback, usages) => {
7869
8785
  });
7870
8786
  return didFindCleanupReturn;
7871
8787
  };
8788
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8789
+ const isSelfReleasingListenerOptionProperty = (property) => {
8790
+ if (!isNodeOfType(property, "Property")) return false;
8791
+ const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8792
+ if (keyName === "signal") return true;
8793
+ if (keyName !== "once") return false;
8794
+ return isNodeOfType(property.value, "Literal") && property.value.value === true;
8795
+ };
8796
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
8797
+ const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8798
+ ["addEventListener", new Set(["removeEventListener", "abort"])],
8799
+ ["addListener", new Set([
8800
+ "removeListener",
8801
+ "off",
8802
+ "abort"
8803
+ ])],
8804
+ ["on", new Set([
8805
+ "off",
8806
+ "removeListener",
8807
+ "on"
8808
+ ])],
8809
+ ["subscribe", new Set(["unsubscribe", "unsub"])],
8810
+ ["sub", new Set(["unsub", "unsubscribe"])],
8811
+ ["watch", new Set(["unwatch", "close"])],
8812
+ ["listen", new Set(["unlisten", "close"])],
8813
+ [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8814
+ ]);
8815
+ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8816
+ "cleanup",
8817
+ "dispose",
8818
+ "destroy",
8819
+ "teardown"
8820
+ ]);
8821
+ const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8822
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8823
+ const getReleaseVerbName = (node) => {
8824
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8825
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8826
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8827
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8828
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8829
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8830
+ return null;
8831
+ };
8832
+ const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8833
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8834
+ let didFindPairedRelease = false;
8835
+ walkAst(body, (child) => {
8836
+ if (didFindPairedRelease) return false;
8837
+ if (child !== body && isFunctionLike$1(child)) return false;
8838
+ const releaseVerbName = getReleaseVerbName(child);
8839
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8840
+ didFindPairedRelease = true;
8841
+ return false;
8842
+ }
8843
+ });
8844
+ return didFindPairedRelease;
8845
+ };
8846
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8847
+ const collectFileReleaseVerbNames = (anyNode) => {
8848
+ let programNode = anyNode;
8849
+ while (programNode.parent) programNode = programNode.parent;
8850
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8851
+ if (cached) return cached;
8852
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8853
+ walkAst(programNode, (child) => {
8854
+ const releaseVerbName = getReleaseVerbName(child);
8855
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8856
+ });
8857
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8858
+ return releaseVerbNames;
8859
+ };
8860
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8861
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8862
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8863
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8864
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8865
+ return false;
8866
+ };
8867
+ const findRetainedFunctionLeak = (retainedFunction) => {
8868
+ if (!isFunctionLike$1(retainedFunction)) return null;
8869
+ const body = retainedFunction.body;
8870
+ if (!body) return null;
8871
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8872
+ let leak = null;
8873
+ walkAst(body, (child) => {
8874
+ if (leak !== null) return false;
8875
+ if (isFunctionLike$1(child)) return false;
8876
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8877
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8878
+ leak = {
8879
+ kind: "socket",
8880
+ node: child,
8881
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8882
+ };
8883
+ return false;
8884
+ }
8885
+ if (!isNodeOfType(child, "CallExpression")) return;
8886
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8887
+ leak = {
8888
+ kind: "timer",
8889
+ node: child,
8890
+ resourceName: "setInterval"
8891
+ };
8892
+ return false;
8893
+ }
8894
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8895
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8896
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8897
+ kind: "subscribe",
8898
+ node: child,
8899
+ resourceName: registrationVerbName
8900
+ };
8901
+ return false;
8902
+ }
8903
+ });
8904
+ return leak;
8905
+ };
8906
+ const isRetainedComponentScopeFunction = (functionNode) => {
8907
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8908
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8909
+ if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8910
+ return enclosingComponentOrHookName(functionNode) !== null;
8911
+ };
7872
8912
  const effectNeedsCleanup = defineRule({
7873
8913
  id: "effect-needs-cleanup",
7874
8914
  title: "Effect subscription or timer never cleaned up",
7875
8915
  severity: "error",
7876
8916
  tags: ["test-noise"],
7877
- recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
7878
- create: (context) => ({ CallExpression(node) {
7879
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7880
- const callback = getEffectCallback(node);
7881
- if (!callback) return;
7882
- const usages = findSubscribeLikeUsages(callback);
7883
- if (usages.length === 0) return;
7884
- if (effectHasCleanupReturn(callback, usages)) return;
7885
- const firstUsage = usages[0];
7886
- const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7887
- context.report({
7888
- node,
7889
- message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7890
- });
7891
- } })
8917
+ recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
8918
+ create: (context) => {
8919
+ const reportRetainedLeak = (retainedFunction) => {
8920
+ const leak = findRetainedFunctionLeak(retainedFunction);
8921
+ if (!leak) return;
8922
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8923
+ context.report({
8924
+ node: leak.node,
8925
+ message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
8926
+ });
8927
+ };
8928
+ return {
8929
+ CallExpression(node) {
8930
+ if (isHookCall$2(node, "useCallback")) {
8931
+ const retainedCallback = getEffectCallback(node);
8932
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
8933
+ return;
8934
+ }
8935
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8936
+ const callback = getEffectCallback(node);
8937
+ if (!callback) return;
8938
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8939
+ if (usages.length === 0) return;
8940
+ if (effectHasCleanupReturn(callback, usages)) return;
8941
+ const firstUsage = usages[0];
8942
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8943
+ context.report({
8944
+ node,
8945
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8946
+ });
8947
+ },
8948
+ FunctionDeclaration(node) {
8949
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8950
+ },
8951
+ ArrowFunctionExpression(node) {
8952
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8953
+ },
8954
+ FunctionExpression(node) {
8955
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8956
+ }
8957
+ };
8958
+ }
7892
8959
  });
7893
8960
  //#endregion
7894
8961
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8447,17 +9514,6 @@ const closureCaptures = (functionNode, scopes) => {
8447
9514
  return computedCaptures;
8448
9515
  };
8449
9516
  //#endregion
8450
- //#region src/plugin/utils/is-react-hook-name.ts
8451
- const isReactHookName = (name) => {
8452
- if (!name.startsWith("use")) return false;
8453
- if (name.length === 3) return true;
8454
- const fourthCharacter = name.charCodeAt(3);
8455
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8456
- };
8457
- //#endregion
8458
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
8459
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8460
- //#endregion
8461
9517
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8462
9518
  const reactHocCalleeName = (callee) => {
8463
9519
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -8671,8 +9727,8 @@ const isAstDescendant = (inner, outer) => {
8671
9727
  * One cohesive concept: "given a captured symbol, is its value
8672
9728
  * structurally stable across re-renders (and therefore unnecessary
8673
9729
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8674
- * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
8675
- * `isRecursiveInitializerCapture` at multiple sites — extracting
9730
+ * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
9731
+ * multiple sites — extracting
8676
9732
  * them lets the rule body stay focused on the diff-the-captured-vs-
8677
9733
  * declared logic.
8678
9734
  *
@@ -8728,11 +9784,6 @@ const symbolHasStableHookOrigin = (symbol) => {
8728
9784
  }
8729
9785
  return false;
8730
9786
  };
8731
- const symbolHasUseEffectEventOrigin = (symbol) => {
8732
- const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
8733
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
8734
- return getHookName(initializer.callee) === "useEffectEvent";
8735
- };
8736
9787
  const getFunctionValueNode = (symbol) => {
8737
9788
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
8738
9789
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -8818,6 +9869,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
8818
9869
  };
8819
9870
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
8820
9871
  //#endregion
9872
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
9873
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9874
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
9875
+ if (importSource === null) return false;
9876
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9877
+ };
9878
+ //#endregion
9879
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9880
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9881
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9882
+ return Boolean(symbol && symbol.kind !== "import");
9883
+ };
9884
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9885
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9886
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9887
+ return false;
9888
+ };
9889
+ //#endregion
9890
+ //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9891
+ const getUseEffectEventCalleeName = (callee) => {
9892
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9893
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9894
+ return null;
9895
+ };
9896
+ const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9897
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9898
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9899
+ if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9900
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9901
+ };
9902
+ //#endregion
8821
9903
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
8822
9904
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
8823
9905
  "useEffect",
@@ -9468,7 +10550,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9468
10550
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9469
10551
  if (isNodeOfType(stripped, "Identifier")) {
9470
10552
  const depSymbol = context.scopes.symbolFor(stripped);
9471
- if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
10553
+ if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9472
10554
  context.report({
9473
10555
  node: elementNode,
9474
10556
  message: buildEffectEventDepMessage()
@@ -9894,7 +10976,10 @@ const PRAGMA = "React";
9894
10976
  const isReactFunctionCall = (node, expectedCall) => {
9895
10977
  if (!isNodeOfType(node, "CallExpression")) return false;
9896
10978
  if (getCalleeName$2(node) !== expectedCall) return false;
9897
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10979
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10980
+ const receiver = stripParenExpression(node.callee.object);
10981
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10982
+ }
9898
10983
  return true;
9899
10984
  };
9900
10985
  //#endregion
@@ -11059,8 +12144,8 @@ const interactiveSupportsFocus = defineRule({
11059
12144
  if (node.attributes.length === 0) return;
11060
12145
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11061
12146
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11062
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11063
- if (!role) return;
12147
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
12148
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11064
12149
  let hasInteractiveHandler = false;
11065
12150
  for (const attribute of node.attributes) {
11066
12151
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11074,11 +12159,16 @@ const interactiveSupportsFocus = defineRule({
11074
12159
  const elementType = getElementType(node, context.settings);
11075
12160
  if (!HTML_TAGS.has(elementType)) return;
11076
12161
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11077
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11078
- if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11079
12162
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11080
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11081
- const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
12163
+ const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
12164
+ for (const role of roleCandidates) {
12165
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
12166
+ if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
12167
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
12168
+ }
12169
+ const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
12170
+ const roleDisplay = roleCandidates.join("' / '");
12171
+ const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11082
12172
  context.report({
11083
12173
  node,
11084
12174
  message
@@ -11267,16 +12357,6 @@ const collectHandlerReferencedNames = (root) => {
11267
12357
  return names;
11268
12358
  };
11269
12359
  //#endregion
11270
- //#region src/plugin/utils/find-enclosing-function.ts
11271
- const findEnclosingFunction = (node) => {
11272
- let cursor = node.parent;
11273
- while (cursor) {
11274
- if (isFunctionLike$1(cursor)) return cursor;
11275
- cursor = cursor.parent ?? null;
11276
- }
11277
- return null;
11278
- };
11279
- //#endregion
11280
12360
  //#region src/plugin/utils/get-function-binding-name.ts
11281
12361
  const getFunctionBindingIdentifier = (functionNode) => {
11282
12362
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -11480,7 +12560,7 @@ const jotaiTqUseRawQueryAtom = defineRule({
11480
12560
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
11481
12561
  if (!isNodeOfType(specifier.local, "Identifier")) continue;
11482
12562
  if (source === "jotai-tanstack-query") {
11483
- const importedName = getImportedName$1(specifier);
12563
+ const importedName = getImportedName(specifier);
11484
12564
  if (importedName && QUERY_ATOM_FACTORY_IMPORTED_NAMES.has(importedName)) queryAtomFactoryLocalNames.add(specifier.local.name);
11485
12565
  continue;
11486
12566
  }
@@ -11976,14 +13056,15 @@ const jsCacheStorage = defineRule({
11976
13056
  "ArrowFunctionExpression:exit": exitFunctionScope,
11977
13057
  CallExpression(node) {
11978
13058
  if (!isMemberProperty(node.callee, "getItem")) return;
11979
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
13059
+ const receiver = stripParenExpression(node.callee.object);
13060
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
11980
13061
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
11981
13062
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
11982
13063
  const storageKey = String(node.arguments[0].value);
11983
13064
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
11984
13065
  storageReadCounts.set(storageKey, readCount);
11985
13066
  if (readCount === 2) {
11986
- const storageName = node.callee.object.name;
13067
+ const storageName = receiver.name;
11987
13068
  context.report({
11988
13069
  node,
11989
13070
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -11997,13 +13078,16 @@ const jsCacheStorage = defineRule({
11997
13078
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
11998
13079
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
11999
13080
  const callee = callExpression.callee;
12000
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12001
- if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12002
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12003
- const receiver = callee.object;
12004
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12005
- return true;
13081
+ if (isNodeOfType(callee, "MemberExpression")) {
13082
+ const receiver = stripParenExpression(callee.object);
13083
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
13084
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
13085
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
13086
+ return true;
13087
+ }
13088
+ return false;
12006
13089
  }
13090
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12007
13091
  return false;
12008
13092
  };
12009
13093
  const isChainPassThroughCall = (callExpression) => {
@@ -12015,10 +13099,7 @@ const isChainPassThroughCall = (callExpression) => {
12015
13099
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12016
13100
  let cursor = receiverNode;
12017
13101
  while (cursor) {
12018
- if (isNodeOfType(cursor, "ChainExpression")) {
12019
- cursor = cursor.expression;
12020
- continue;
12021
- }
13102
+ cursor = stripParenExpression(cursor);
12022
13103
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12023
13104
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12024
13105
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12094,10 +13175,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12094
13175
  let hops = 0;
12095
13176
  while (cursor && hops < 12) {
12096
13177
  hops += 1;
12097
- if (isNodeOfType(cursor, "ChainExpression")) {
12098
- cursor = cursor.expression;
12099
- continue;
12100
- }
13178
+ cursor = stripParenExpression(cursor);
12101
13179
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12102
13180
  const callee = cursor.callee;
12103
13181
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12121,10 +13199,7 @@ const isSmallLiteralArray = (node) => {
12121
13199
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12122
13200
  let cursor = receiverNode;
12123
13201
  while (cursor) {
12124
- if (isNodeOfType(cursor, "ChainExpression")) {
12125
- cursor = cursor.expression;
12126
- continue;
12127
- }
13202
+ cursor = stripParenExpression(cursor);
12128
13203
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12129
13204
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12130
13205
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12189,7 +13264,7 @@ const jsCombineIterations = defineRule({
12189
13264
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12190
13265
  const outerMethod = node.callee.property.name;
12191
13266
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12192
- const innerCall = node.callee.object;
13267
+ const innerCall = stripParenExpression(node.callee.object);
12193
13268
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12194
13269
  const innerMethod = innerCall.callee.property.name;
12195
13270
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12262,10 +13337,10 @@ const jsFlatmapFilter = defineRule({
12262
13337
  if (!filterArgument) return;
12263
13338
  const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
12264
13339
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12265
- const innerCall = node.callee.object;
13340
+ const innerCall = stripParenExpression(node.callee.object);
12266
13341
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12267
13342
  if (innerCall.callee.property.name !== "map") return;
12268
- const receiver = innerCall.callee.object;
13343
+ const receiver = stripParenExpression(innerCall.callee.object);
12269
13344
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12270
13345
  const elements = receiver.elements ?? [];
12271
13346
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13525,7 +14600,7 @@ const jsTosortedImmutable = defineRule({
13525
14600
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13526
14601
  create: (context) => ({ CallExpression(node) {
13527
14602
  if (!isMemberProperty(node.callee, "sort")) return;
13528
- const receiver = node.callee.object;
14603
+ const receiver = stripParenExpression(node.callee.object);
13529
14604
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13530
14605
  const spreadArgument = receiver.elements[0].argument;
13531
14606
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18562,7 +19637,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18562
19637
  };
18563
19638
  const describeClientSideNavigation = (node) => {
18564
19639
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18565
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19640
+ const receiver = stripParenExpression(node.callee.object);
19641
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18566
19642
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18567
19643
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18568
19644
  }
@@ -19182,7 +20258,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19182
20258
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19183
20259
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19184
20260
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19185
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
20261
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19186
20262
  return node.callee.property.name;
19187
20263
  };
19188
20264
  const isMutatingFetchCall = (node) => {
@@ -19196,7 +20272,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19196
20272
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19197
20273
  const { property, object } = node.callee;
19198
20274
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19199
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
20275
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19200
20276
  return true;
19201
20277
  };
19202
20278
  const getDbCallDescription = (node) => {
@@ -19204,7 +20280,8 @@ const getDbCallDescription = (node) => {
19204
20280
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19205
20281
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19206
20282
  const methodName = node.callee.property.name;
19207
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
20283
+ const receiver = stripParenExpression(node.callee.object);
20284
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19208
20285
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19209
20286
  };
19210
20287
  const findSideEffect = (node, options = {}) => {
@@ -20089,7 +21166,7 @@ const astMentionsSuspense = (programNode) => {
20089
21166
  return false;
20090
21167
  }
20091
21168
  if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
20092
- if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
21169
+ if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense")) {
20093
21170
  didDetect = true;
20094
21171
  return false;
20095
21172
  }
@@ -20133,7 +21210,7 @@ const collectSuspenseLocalNames = (programNode) => {
20133
21210
  for (const statement of programNode.body ?? []) {
20134
21211
  if (!isNodeOfType(statement, "ImportDeclaration")) continue;
20135
21212
  if (statement.source?.value !== "react") continue;
20136
- for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
21213
+ for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense" && specifier.local?.name) names.add(specifier.local.name);
20137
21214
  }
20138
21215
  return names;
20139
21216
  };
@@ -20604,13 +21681,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20604
21681
  if (isNodeOfType(current, "CallExpression")) {
20605
21682
  let node = ref.identifier;
20606
21683
  let parent = node.parent;
20607
- while (parent && isNodeOfType(parent, "MemberExpression")) {
21684
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20608
21685
  node = parent;
20609
21686
  parent = node.parent;
20610
21687
  }
20611
21688
  if (current.callee === node) return current;
20612
21689
  }
20613
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
21690
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20614
21691
  return null;
20615
21692
  };
20616
21693
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20745,7 +21822,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20745
21822
  const importDeclaration = declarationNode.parent;
20746
21823
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20747
21824
  }));
20748
- const isHookCallee = (analysis, node, hookName) => {
21825
+ const isHookCallee$1 = (analysis, node, hookName) => {
20749
21826
  if (!node) return false;
20750
21827
  if (isNodeOfType(node, "Identifier")) {
20751
21828
  if (node.name === hookName) return true;
@@ -20754,15 +21831,19 @@ const isHookCallee = (analysis, node, hookName) => {
20754
21831
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20755
21832
  return false;
20756
21833
  }
20757
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21834
+ if (isNodeOfType(node, "MemberExpression")) {
21835
+ const receiver = stripParenExpression(node.object);
21836
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21837
+ }
20758
21838
  return false;
20759
21839
  };
20760
21840
  const isUseEffect = (node) => {
20761
21841
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20762
21842
  const callee = node.callee;
20763
21843
  if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
20764
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
20765
- return false;
21844
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21845
+ const receiver = stripParenExpression(callee.object);
21846
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20766
21847
  };
20767
21848
  const getEffectFn = (analysis, node) => {
20768
21849
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20790,7 +21871,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20790
21871
  const node = def.node;
20791
21872
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20792
21873
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20793
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21874
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20794
21875
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20795
21876
  const elements = node.id.elements ?? [];
20796
21877
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20801,7 +21882,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20801
21882
  const node = def.node;
20802
21883
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20803
21884
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20804
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21885
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20805
21886
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20806
21887
  const elements = node.id.elements ?? [];
20807
21888
  if (elements.length !== 2) return false;
@@ -20848,7 +21929,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20848
21929
  const node = def.node;
20849
21930
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20850
21931
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20851
- return isHookCallee(analysis, node.init.callee, "useRef");
21932
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20852
21933
  }));
20853
21934
  const isRefCurrent = (ref) => {
20854
21935
  const parent = ref.identifier.parent;
@@ -20861,11 +21942,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20861
21942
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20862
21943
  const isPropCallbackInvocationRef = (analysis, ref) => {
20863
21944
  if (!isPropAlias(analysis, ref)) return false;
20864
- const identifier = ref.identifier;
20865
- const parent = identifier.parent;
21945
+ let effectiveNode = ref.identifier;
21946
+ let parent = effectiveNode.parent;
21947
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21948
+ effectiveNode = parent;
21949
+ parent = effectiveNode.parent;
21950
+ }
20866
21951
  if (!parent) return false;
20867
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20868
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21952
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21953
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20869
21954
  const memberParent = parent.parent;
20870
21955
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20871
21956
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20876,7 +21961,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20876
21961
  };
20877
21962
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20878
21963
  const getUseStateDecl = (analysis, ref) => {
20879
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21964
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20880
21965
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20881
21966
  return node ?? null;
20882
21967
  };
@@ -21081,7 +22166,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21081
22166
  const noAdjustStateOnPropChange = defineRule({
21082
22167
  id: "no-adjust-state-on-prop-change",
21083
22168
  title: "State synced to a prop inside an effect",
21084
- severity: "error",
22169
+ severity: "warn",
21085
22170
  tags: ["test-noise"],
21086
22171
  recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
21087
22172
  create: (context) => ({ CallExpression(node) {
@@ -21122,7 +22207,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21122
22207
  "summary",
21123
22208
  "textarea"
21124
22209
  ]);
22210
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
22211
+ const value = attribute.value;
22212
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
22213
+ const expression = value.expression;
22214
+ return isNodeOfType(expression, "Literal") && expression.value === false;
22215
+ };
22216
+ const DISABLEABLE_TAGS = new Set([
22217
+ "button",
22218
+ "input",
22219
+ "select",
22220
+ "textarea"
22221
+ ]);
21125
22222
  const isNativelyFocusable = (tagName, openingElement) => {
22223
+ if (DISABLEABLE_TAGS.has(tagName)) {
22224
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
22225
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
22226
+ }
21126
22227
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21127
22228
  switch (tagName) {
21128
22229
  case "input": {
@@ -21136,7 +22237,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21136
22237
  case "a":
21137
22238
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21138
22239
  case "audio":
21139
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
22240
+ case "video": {
22241
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
22242
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
22243
+ }
21140
22244
  default: return false;
21141
22245
  }
21142
22246
  };
@@ -22100,7 +23204,8 @@ const SECOND_INDEX_METHODS = new Set([
22100
23204
  "some"
22101
23205
  ]);
22102
23206
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22103
- const isPositionallyStableIterationReceiver = (receiver) => {
23207
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
23208
+ const receiver = stripParenExpression(receiverNode);
22104
23209
  if (isAllLiteralArrayExpression(receiver)) return true;
22105
23210
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22106
23211
  const only = receiver.elements[0];
@@ -22111,17 +23216,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22111
23216
  }
22112
23217
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22113
23218
  const callee = receiver.callee;
22114
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
23219
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22115
23220
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22116
23221
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22117
23222
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22118
23223
  return false;
22119
23224
  };
22120
- const isArrayFromMapperCallback = (parentCall, callback) => {
22121
- if (parentCall.arguments[1] !== callback) return false;
22122
- const callee = parentCall.callee;
22123
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
22124
- };
23225
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22125
23226
  const isArrayFromSourcePositionallyStable = (source) => {
22126
23227
  if (isNodeOfType(source, "ObjectExpression")) {
22127
23228
  for (const property of source.properties ?? []) {
@@ -22180,18 +23281,12 @@ const expressionUsesIndex = (expression, paramName) => {
22180
23281
  return false;
22181
23282
  }
22182
23283
  if (isNodeOfType(expression, "CallExpression")) {
22183
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
23284
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22184
23285
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22185
23286
  }
22186
23287
  return false;
22187
23288
  };
22188
- const isReactCloneElement = (callExpression) => {
22189
- const callee = callExpression.callee;
22190
- if (!isNodeOfType(callee, "MemberExpression")) return false;
22191
- if (!isNodeOfType(callee.property, "Identifier")) return false;
22192
- if (callee.property.name !== "cloneElement") return false;
22193
- return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
22194
- };
23289
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22195
23290
  const noArrayIndexKey = defineRule({
22196
23291
  id: "no-array-index-key",
22197
23292
  title: "Array index used as a key",
@@ -22700,6 +23795,10 @@ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
22700
23795
  "ClassDeclaration",
22701
23796
  "ClassExpression"
22702
23797
  ]);
23798
+ const REACT_CREATE_ELEMENT_OPTIONS = {
23799
+ allowGlobalReactNamespace: false,
23800
+ allowUnboundBareCalls: false
23801
+ };
22703
23802
  const isCallArgumentFunctionExpression = (node) => {
22704
23803
  if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return false;
22705
23804
  const parent = node.parent;
@@ -22707,44 +23806,10 @@ const isCallArgumentFunctionExpression = (node) => {
22707
23806
  return parent.arguments.some((argumentNode) => argumentNode === node);
22708
23807
  };
22709
23808
  const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
22710
- const isReactImport$1 = (symbol) => {
22711
- let importDeclaration = symbol.declarationNode?.parent;
22712
- while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
22713
- if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
22714
- return importDeclaration.source.value === "react";
22715
- };
22716
- const getImportedName = (symbol) => {
22717
- if (symbol.kind !== "import") return null;
22718
- if (!isReactImport$1(symbol)) return null;
22719
- return getImportedName$1(symbol.declarationNode) ?? null;
22720
- };
22721
- const isReactNamespaceImport = (symbol) => {
22722
- if (symbol.kind !== "import") return false;
22723
- if (!isReactImport$1(symbol)) return false;
22724
- return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
22725
- };
22726
- const isReactCreateElementIdentifierCall = (callee, scopes) => {
22727
- if (!isNodeOfType(callee, "Identifier")) return false;
22728
- const symbol = scopes.symbolFor(callee);
22729
- return Boolean(symbol && getImportedName(symbol) === "createElement");
22730
- };
22731
- const isReactCreateElementMemberCall = (callee, scopes) => {
22732
- if (!isNodeOfType(callee, "MemberExpression")) return false;
22733
- if (callee.computed) return false;
22734
- if (!isNodeOfType(callee.object, "Identifier")) return false;
22735
- if (!isNodeOfType(callee.property, "Identifier")) return false;
22736
- if (callee.property.name !== "createElement") return false;
22737
- const symbol = scopes.symbolFor(callee.object);
22738
- return Boolean(symbol && isReactNamespaceImport(symbol));
22739
- };
22740
- const isReactCreateElementCall = (node, scopes) => {
22741
- if (!isNodeOfType(node, "CallExpression")) return false;
22742
- return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
22743
- };
22744
23809
  const containsRenderOutput$1 = (node, rootNode, scopes) => {
22745
23810
  if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
22746
23811
  if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
22747
- if (isReactCreateElementCall(node, scopes)) return true;
23812
+ if (isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) return true;
22748
23813
  const nodeRecord = node;
22749
23814
  for (const key of Object.keys(nodeRecord)) {
22750
23815
  if (key === "parent") continue;
@@ -22915,7 +23980,7 @@ const isAsyncFunctionLike = (node) => {
22915
23980
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22916
23981
  return false;
22917
23982
  };
22918
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23983
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22919
23984
  "forEach",
22920
23985
  "map",
22921
23986
  "filter",
@@ -22936,30 +24001,51 @@ const runsOnEffectDispatch = (functionNode) => {
22936
24001
  if (parent.callee === functionNode) return true;
22937
24002
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22938
24003
  const callee = parent.callee;
22939
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
24004
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22940
24005
  };
22941
24006
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
22942
- const isGuardWithTerminatingBranch = (statement) => {
22943
- if (!isNodeOfType(statement, "IfStatement")) return null;
22944
- if (statement.alternate) return null;
22945
- const consequent = statement.consequent;
22946
- if (isTerminatingStatement(consequent)) return consequent;
22947
- if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
22948
- return null;
22949
- };
22950
- const countStatementSequenceSetStateCalls = (statements, context) => {
24007
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
24008
+ const analyzeStatementSequence = (statements, context) => {
22951
24009
  let fallThroughCount = 0;
22952
- let maxTerminatingPathCount = 0;
24010
+ let maxTerminatedCount = 0;
22953
24011
  for (const statement of statements) {
22954
- const guardBranch = isGuardWithTerminatingBranch(statement);
22955
- if (guardBranch) {
22956
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
24012
+ if (isNodeOfType(statement, "IfStatement")) {
24013
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
24014
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
24015
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
24016
+ fallThroughCount: 0,
24017
+ maxTerminatedCount: 0,
24018
+ doAllPathsTerminate: false
24019
+ };
24020
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
24021
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
24022
+ fallThroughCount: 0,
24023
+ maxTerminatedCount,
24024
+ doAllPathsTerminate: true
24025
+ };
24026
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
24027
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22957
24028
  continue;
22958
24029
  }
22959
- if (isTerminatingStatement(statement)) break;
24030
+ if (isTerminatingStatement(statement)) {
24031
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
24032
+ return {
24033
+ fallThroughCount: 0,
24034
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
24035
+ doAllPathsTerminate: true
24036
+ };
24037
+ }
22960
24038
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22961
24039
  }
22962
- return Math.max(maxTerminatingPathCount, fallThroughCount);
24040
+ return {
24041
+ fallThroughCount,
24042
+ maxTerminatedCount,
24043
+ doAllPathsTerminate: false
24044
+ };
24045
+ };
24046
+ const countStatementSequenceSetStateCalls = (statements, context) => {
24047
+ const summary = analyzeStatementSequence(statements, context);
24048
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22963
24049
  };
22964
24050
  const collectLocalHelperFunctions = (root) => {
22965
24051
  const helpersByName = /* @__PURE__ */ new Map();
@@ -23090,7 +24176,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23090
24176
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23091
24177
  const firstStatement = (body.body ?? [])[0];
23092
24178
  if (!firstStatement) return false;
23093
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
24179
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
24180
+ const consequent = firstStatement.consequent;
24181
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23094
24182
  return mentionsDevEnvFlag(firstStatement.test);
23095
24183
  };
23096
24184
  const noCascadingSetState = defineRule({
@@ -23449,40 +24537,6 @@ const noCloneElement = defineRule({
23449
24537
  } })
23450
24538
  });
23451
24539
  //#endregion
23452
- //#region src/plugin/utils/component-or-hook-display-name.ts
23453
- const hocWrapperCalleeName = (callee) => {
23454
- if (isNodeOfType(callee, "Identifier")) return callee.name;
23455
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23456
- return null;
23457
- };
23458
- const displayNameFromFunctionBinding = (functionNode) => {
23459
- let current = functionNode;
23460
- for (;;) {
23461
- const parent = current.parent;
23462
- if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23463
- const calleeName = hocWrapperCalleeName(parent.callee);
23464
- if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23465
- current = parent;
23466
- continue;
23467
- }
23468
- }
23469
- break;
23470
- }
23471
- const binding = current.parent;
23472
- if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23473
- return null;
23474
- };
23475
- const componentOrHookDisplayNameForFunction = (functionNode) => {
23476
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23477
- return displayNameFromFunctionBinding(functionNode);
23478
- };
23479
- //#endregion
23480
- //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23481
- const enclosingComponentOrHookName = (node) => {
23482
- const functionNode = findEnclosingFunction(node);
23483
- return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23484
- };
23485
- //#endregion
23486
24540
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23487
24541
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23488
24542
  const CONTEXT_MODULES = [
@@ -24071,16 +25125,6 @@ const isInitialOnlyPropName = (propName) => {
24071
25125
  return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
24072
25126
  };
24073
25127
  //#endregion
24074
- //#region src/plugin/rules/state-and-effects/utils/static-member-property-name.ts
24075
- const getStaticMemberPropertyName = (node) => {
24076
- if (!node) return null;
24077
- const unwrappedNode = stripParenExpression(node);
24078
- if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
24079
- if (!unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier")) return unwrappedNode.property.name;
24080
- if (unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Literal") && typeof unwrappedNode.property.value === "string") return unwrappedNode.property.value;
24081
- return null;
24082
- };
24083
- //#endregion
24084
25128
  //#region src/plugin/rules/state-and-effects/utils/event-handler-reference.ts
24085
25129
  const isEventHandlerName = (name) => /^on[A-Z]/.test(name);
24086
25130
  const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
@@ -24089,13 +25133,6 @@ const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
24089
25133
  const propertyName = getStaticMemberPropertyName(node);
24090
25134
  return propertyName ? `${resolveName(node.object.name)}.${propertyName}` : null;
24091
25135
  };
24092
- const getStaticPropertyKeyName = (node) => {
24093
- if (!isNodeOfType(node, "Property")) return null;
24094
- if (node.computed) return null;
24095
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
24096
- if (isNodeOfType(node.key, "Literal")) return String(node.key.value);
24097
- return null;
24098
- };
24099
25136
  const isEventHandlerValue = (node, eventHandlerReferenceNames, resolveName = (name) => name) => {
24100
25137
  if (isInlineFunctionExpression(node)) return true;
24101
25138
  if (isNodeOfType(node, "Identifier")) return eventHandlerReferenceNames.has(resolveName(node.name));
@@ -24453,11 +25490,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24453
25490
  return isInitialOnlyPropName(arg.name);
24454
25491
  };
24455
25492
  //#endregion
25493
+ //#region src/plugin/utils/is-no-op-statement.ts
25494
+ const isNoOpStatement = (statement) => {
25495
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
25496
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
25497
+ const expression = stripParenExpression(statement.expression);
25498
+ if (isNodeOfType(expression, "Literal")) return true;
25499
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
25500
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
25501
+ return false;
25502
+ };
25503
+ //#endregion
24456
25504
  //#region src/plugin/utils/get-callback-statements.ts
24457
25505
  const getCallbackStatements = (callback) => {
24458
25506
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
24459
- if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
24460
- return callback.body ? [callback.body] : [];
25507
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24461
25508
  };
24462
25509
  //#endregion
24463
25510
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24496,6 +25543,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24496
25543
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24497
25544
  }
24498
25545
  };
25546
+ const flattenGuardedStatements = (statements) => {
25547
+ const flattened = [];
25548
+ for (const statement of statements) {
25549
+ if (isNoOpStatement(statement)) continue;
25550
+ if (isNodeOfType(statement, "ExpressionStatement")) {
25551
+ flattened.push(statement);
25552
+ continue;
25553
+ }
25554
+ if (isNodeOfType(statement, "IfStatement")) {
25555
+ for (const branch of [statement.consequent, statement.alternate]) {
25556
+ if (!branch) continue;
25557
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
25558
+ if (flattenedBranch === null) return null;
25559
+ flattened.push(...flattenedBranch);
25560
+ }
25561
+ continue;
25562
+ }
25563
+ return null;
25564
+ }
25565
+ return flattened;
25566
+ };
24499
25567
  const noDerivedStateEffect = defineRule({
24500
25568
  id: "no-derived-state-effect",
24501
25569
  title: "Derived state stored in an effect",
@@ -24527,8 +25595,8 @@ const noDerivedStateEffect = defineRule({
24527
25595
  }
24528
25596
  }
24529
25597
  if (sawAnyDep && allDepsAreInitialOnly) return;
24530
- const statements = getCallbackStatements(callback);
24531
- if (statements.length === 0) return;
25598
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
25599
+ if (statements === null || statements.length === 0) return;
24532
25600
  if (!statements.every((statement) => {
24533
25601
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24534
25602
  const expression = statement.expression;
@@ -25163,7 +26231,7 @@ const noDidMountSetState = defineRule({
25163
26231
  const { mode } = resolveSettings$20(context.settings);
25164
26232
  return { CallExpression(node) {
25165
26233
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25166
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
26234
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25167
26235
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25168
26236
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25169
26237
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25288,7 +26356,7 @@ const noDidUpdateSetState = defineRule({
25288
26356
  const { mode } = resolveSettings$19(context.settings);
25289
26357
  return { CallExpression(node) {
25290
26358
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25291
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
26359
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25292
26360
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25293
26361
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25294
26362
  if (isInsideDiffGuard(node)) return;
@@ -25385,18 +26453,45 @@ const collectUseStateBindings = (componentBody) => {
25385
26453
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25386
26454
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25387
26455
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
26456
+ const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
26457
+ const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
26458
+ "assign",
26459
+ "entries",
26460
+ "fromEntries",
26461
+ "keys",
26462
+ "values"
26463
+ ]);
26464
+ const ARRAY_COPY_METHOD_NAMES = new Set([
26465
+ "map",
26466
+ "filter",
26467
+ "slice",
26468
+ "concat",
26469
+ "flat",
26470
+ "flatMap",
26471
+ "toSorted",
26472
+ "toReversed",
26473
+ "toSpliced",
26474
+ "with"
26475
+ ]);
25388
26476
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25389
26477
  const isPlainDataProducerCall = (expression) => {
25390
26478
  if (!isNodeOfType(expression, "CallExpression")) return false;
25391
26479
  const callee = expression.callee;
25392
26480
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25393
26481
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
25394
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
26482
+ if (isNodeOfType(callee.object, "Identifier")) {
26483
+ if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
26484
+ if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
26485
+ if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
26486
+ }
25395
26487
  return producesPlainStateValue(callee.object);
25396
26488
  };
26489
+ const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
26490
+ const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25397
26491
  const producesPlainStateValue = (expression) => {
25398
26492
  const unwrapped = stripParenExpression(expression);
25399
26493
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
26494
+ if (isPlainDataNewExpression(unwrapped)) return true;
25400
26495
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25401
26496
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25402
26497
  return isPlainDataProducerCall(unwrapped);
@@ -25411,6 +26506,38 @@ const initializerMarksPlainState = (initializerArgument) => {
25411
26506
  }
25412
26507
  return producesPlainStateValue(unwrapped);
25413
26508
  };
26509
+ const producesOpaqueInstanceValue = (expression) => {
26510
+ if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
26511
+ if (!isNodeOfType(expression, "CallExpression")) return false;
26512
+ const callee = expression.callee;
26513
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
26514
+ if (isPlainDataProducerCall(expression)) return false;
26515
+ if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
26516
+ return true;
26517
+ };
26518
+ const collectSetterValueObservations = (componentBody, setterNames) => {
26519
+ const plainFedSetterNames = /* @__PURE__ */ new Set();
26520
+ const opaqueFedSetterNames = /* @__PURE__ */ new Set();
26521
+ walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
26522
+ if (!isNodeOfType(node, "CallExpression")) return;
26523
+ if (!isNodeOfType(node.callee, "Identifier")) return;
26524
+ const setterName = node.callee.name;
26525
+ if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
26526
+ const argument = node.arguments?.[0];
26527
+ if (!argument) return;
26528
+ const unwrapped = stripParenExpression(argument);
26529
+ if (isNullOrUndefinedExpression(unwrapped)) return;
26530
+ if (producesPlainStateValue(unwrapped)) {
26531
+ plainFedSetterNames.add(setterName);
26532
+ return;
26533
+ }
26534
+ if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
26535
+ }, true);
26536
+ return {
26537
+ plainFedSetterNames,
26538
+ opaqueFedSetterNames
26539
+ };
26540
+ };
25414
26541
  const collectCallbackRefSetterNames = (componentBody) => {
25415
26542
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25416
26543
  walkAst(componentBody, (node) => {
@@ -25480,11 +26607,15 @@ const noDirectStateMutation = defineRule({
25480
26607
  if (bindings.length === 0) return;
25481
26608
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25482
26609
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
26610
+ const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25483
26611
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25484
26612
  for (const binding of bindings) {
25485
26613
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25486
26614
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25487
- if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
26615
+ const initializerArgument = binding.declarator.init.arguments?.[0];
26616
+ if (!initializerMarksPlainState(initializerArgument)) continue;
26617
+ if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
26618
+ plainObjectStateValueNames.add(binding.valueName);
25488
26619
  }
25489
26620
  const visitMutationCandidate = (child, currentlyShadowed) => {
25490
26621
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -25609,9 +26740,10 @@ const noDocumentStartViewTransition = defineRule({
25609
26740
  create: (context) => ({ CallExpression(node) {
25610
26741
  const callee = node.callee;
25611
26742
  if (!isNodeOfType(callee, "MemberExpression")) return;
25612
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26743
+ const receiver = stripParenExpression(callee.object);
26744
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25613
26745
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25614
- if (context.scopes.symbolFor(callee.object) !== null) return;
26746
+ if (context.scopes.symbolFor(receiver) !== null) return;
25615
26747
  if (!importsReactViewTransition(node)) return;
25616
26748
  context.report({
25617
26749
  node,
@@ -25631,7 +26763,8 @@ const noDocumentWrite = defineRule({
25631
26763
  create: (context) => ({ CallExpression(node) {
25632
26764
  const callee = node.callee;
25633
26765
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25634
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26766
+ const receiver = stripParenExpression(callee.object);
26767
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25635
26768
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25636
26769
  context.report({
25637
26770
  node,
@@ -26263,13 +27396,6 @@ const noEffectEventHandler = defineRule({
26263
27396
  }
26264
27397
  });
26265
27398
  //#endregion
26266
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
26267
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
26268
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
26269
- if (importSource === null) return false;
26270
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
26271
- };
26272
- //#endregion
26273
27399
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26274
27400
  const createComponentBindingStackTracker = (callbacks) => {
26275
27401
  const componentBindingStack = [];
@@ -26323,11 +27449,7 @@ const noEffectEventInDeps = defineRule({
26323
27449
  const initializer = declaratorNode.init;
26324
27450
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26325
27451
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26326
- if (isNodeOfType(initializer.callee, "Identifier")) {
26327
- if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26328
- const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26329
- if (calleeSymbol && calleeSymbol.kind !== "import") return;
26330
- }
27452
+ if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26331
27453
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26332
27454
  } });
26333
27455
  return {
@@ -27044,7 +28166,7 @@ const collectVariableDeclarationReferenceNames = (node, scope, eventHandlerRefer
27044
28166
  };
27045
28167
  const collectPropertyReferenceNames = (node, scope, eventHandlerReferenceNames) => {
27046
28168
  if (!isNodeOfType(node, "Property")) return /* @__PURE__ */ new Set();
27047
- const propertyName = getStaticPropertyKeyName(node);
28169
+ const propertyName = getStaticPropertyKeyName(node, { stringifyNonStringLiterals: true });
27048
28170
  if (propertyName && isEventHandlerName(propertyName) && isEventHandlerValue(node.value, eventHandlerReferenceNames, (name) => resolveBindingName(scope, name))) return /* @__PURE__ */ new Set();
27049
28171
  const names = /* @__PURE__ */ new Set();
27050
28172
  if (node.computed) addNames$1(names, collectScopedReferenceNames(node.key, scope, eventHandlerReferenceNames));
@@ -27422,7 +28544,7 @@ const addObjectPropertyFunctionNames = (objectBindingName, node, functionLikeLoc
27422
28544
  if (!isNodeOfType(node, "ObjectExpression")) return;
27423
28545
  for (const property of node.properties ?? []) {
27424
28546
  if (!isNodeOfType(property, "Property")) continue;
27425
- const propertyName = getStaticPropertyKeyName(property);
28547
+ const propertyName = getStaticPropertyKeyName(property, { stringifyNonStringLiterals: true });
27426
28548
  if (!propertyName) continue;
27427
28549
  if (!isFunctionLikeReference(property.value, functionLikeLocalNames, scope)) continue;
27428
28550
  functionLikeLocalNames.add(`${objectBindingName}.${propertyName}`);
@@ -27933,7 +29055,7 @@ const noFlushSync = defineRule({
27933
29055
  if (node.source?.value !== "react-dom") return;
27934
29056
  for (const specifier of node.specifiers ?? []) {
27935
29057
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
27936
- if (getImportedName$1(specifier) !== "flushSync") continue;
29058
+ if (getImportedName(specifier) !== "flushSync") continue;
27937
29059
  const program = findProgram(node);
27938
29060
  if (program) {
27939
29061
  if (importsImperativeDomLibrary(program)) return;
@@ -28907,7 +30029,7 @@ const noIsMounted = defineRule({
28907
30029
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28908
30030
  create: (context) => ({ CallExpression(node) {
28909
30031
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28910
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
30032
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28911
30033
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28912
30034
  if (!getParentComponent(node)) return;
28913
30035
  context.report({
@@ -28922,7 +30044,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28922
30044
  const isJsonMethodCall = (node, method) => {
28923
30045
  if (!isNodeOfType(node, "CallExpression")) return false;
28924
30046
  const callee = node.callee;
28925
- return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
30047
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
30048
+ const receiver = stripParenExpression(callee.object);
30049
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28926
30050
  };
28927
30051
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28928
30052
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -29007,7 +30131,7 @@ const extractReturnTypeAnnotation = (returnType) => {
29007
30131
  const noJsxElementType = defineRule({
29008
30132
  id: "no-jsx-element-type",
29009
30133
  title: "No JSX.Element",
29010
- severity: "error",
30134
+ severity: "warn",
29011
30135
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
29012
30136
  create: (context) => {
29013
30137
  let isJsxImported = false;
@@ -29333,6 +30457,378 @@ const noLegacyContextApi = defineRule({
29333
30457
  }
29334
30458
  });
29335
30459
  //#endregion
30460
+ //#region src/plugin/utils/executes-during-render.ts
30461
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
30462
+ "map",
30463
+ "filter",
30464
+ "forEach",
30465
+ "flatMap",
30466
+ "reduce",
30467
+ "reduceRight",
30468
+ "some",
30469
+ "every",
30470
+ "find",
30471
+ "findIndex",
30472
+ "findLast",
30473
+ "findLastIndex",
30474
+ "sort",
30475
+ "toSorted"
30476
+ ]);
30477
+ const executesDuringRender = (functionNode) => {
30478
+ const parent = functionNode.parent;
30479
+ if (!isNodeOfType(parent, "CallExpression")) return false;
30480
+ if (parent.callee === functionNode) return true;
30481
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
30482
+ 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;
30483
+ };
30484
+ //#endregion
30485
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
30486
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
30487
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
30488
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
30489
+ return false;
30490
+ };
30491
+ //#endregion
30492
+ //#region src/plugin/utils/find-declarator-for-binding.ts
30493
+ const findDeclaratorForBinding = (bindingIdentifier) => {
30494
+ let ancestor = bindingIdentifier.parent;
30495
+ while (ancestor) {
30496
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
30497
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
30498
+ ancestor = ancestor.parent ?? null;
30499
+ }
30500
+ return null;
30501
+ };
30502
+ //#endregion
30503
+ //#region src/plugin/utils/references-falsy-initial-state.ts
30504
+ const isFalsyLiteral = (node) => {
30505
+ if (!node) return true;
30506
+ if (isNodeOfType(node, "Literal")) return !node.value;
30507
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
30508
+ };
30509
+ const isFalsyInitialStateBinding = (identifier) => {
30510
+ if (!isNodeOfType(identifier, "Identifier")) return false;
30511
+ const binding = findVariableInitializer(identifier, identifier.name);
30512
+ if (!binding) return false;
30513
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
30514
+ if (!declarator?.init) return false;
30515
+ const init = stripParenExpression(declarator.init);
30516
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
30517
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
30518
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
30519
+ return isFalsyLiteral(init.arguments?.[0]);
30520
+ };
30521
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
30522
+ //#endregion
30523
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
30524
+ const isGatedByFalsyInitialState = (node) => {
30525
+ let cursor = node;
30526
+ let parent = node.parent;
30527
+ while (parent) {
30528
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
30529
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
30530
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
30531
+ cursor = parent;
30532
+ parent = parent.parent ?? null;
30533
+ }
30534
+ return false;
30535
+ };
30536
+ //#endregion
30537
+ //#region src/plugin/utils/references-client-only-flag.ts
30538
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
30539
+ const referencesClientOnlyFlag = (expression) => {
30540
+ const unwrapped = stripParenExpression(expression);
30541
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
30542
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
30543
+ const property = unwrapped.property;
30544
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
30545
+ }
30546
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
30547
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
30548
+ return false;
30549
+ };
30550
+ //#endregion
30551
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
30552
+ const isInsideClientOnlyGuard = (node) => {
30553
+ let cursor = node;
30554
+ let parent = node.parent;
30555
+ while (parent) {
30556
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
30557
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
30558
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
30559
+ cursor = parent;
30560
+ parent = parent.parent ?? null;
30561
+ }
30562
+ return false;
30563
+ };
30564
+ //#endregion
30565
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
30566
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
30567
+ "toLocaleString",
30568
+ "toLocaleDateString",
30569
+ "toLocaleTimeString"
30570
+ ]);
30571
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
30572
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
30573
+ const INTL_FORMAT_METHOD_NAMES = new Set([
30574
+ "format",
30575
+ "formatToParts",
30576
+ "formatRange"
30577
+ ]);
30578
+ const isProvableDateExpression = (expression) => {
30579
+ if (!expression) return false;
30580
+ const unwrapped = stripParenExpression(expression);
30581
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
30582
+ };
30583
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
30584
+ const receiverNameLooksDateFlavored = (expression) => {
30585
+ if (!expression) return false;
30586
+ const unwrapped = stripParenExpression(expression);
30587
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
30588
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
30589
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
30590
+ return false;
30591
+ };
30592
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
30593
+ if (!objectExpression) return false;
30594
+ const unwrapped = stripParenExpression(objectExpression);
30595
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
30596
+ for (const property of unwrapped.properties ?? []) {
30597
+ if (!isNodeOfType(property, "Property")) continue;
30598
+ if (property.computed) continue;
30599
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
30600
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
30601
+ }
30602
+ return false;
30603
+ };
30604
+ const hasExplicitLocaleArgument = (argument) => {
30605
+ if (!argument) return false;
30606
+ const unwrapped = stripParenExpression(argument);
30607
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
30608
+ return true;
30609
+ };
30610
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
30611
+ const localeArgument = call.arguments?.[0];
30612
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
30613
+ const optionsArgument = call.arguments?.[1];
30614
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
30615
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
30616
+ };
30617
+ const matchLocaleMethodCall = (call) => {
30618
+ const callee = call.callee;
30619
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
30620
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
30621
+ const methodName = callee.property.name;
30622
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
30623
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
30624
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
30625
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
30626
+ return {
30627
+ node: call,
30628
+ display: `${methodName}()`
30629
+ };
30630
+ };
30631
+ const getIntlFormatterName = (expression) => {
30632
+ if (!expression) return null;
30633
+ const unwrapped = stripParenExpression(expression);
30634
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
30635
+ const callee = unwrapped.callee;
30636
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
30637
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
30638
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
30639
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
30640
+ };
30641
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
30642
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
30643
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
30644
+ if (formatterName !== "DateTimeFormat") return true;
30645
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
30646
+ };
30647
+ const matchIntlFormatCall = (call) => {
30648
+ const callee = call.callee;
30649
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
30650
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
30651
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
30652
+ let construction = stripParenExpression(callee.object);
30653
+ if (isNodeOfType(construction, "Identifier")) {
30654
+ const binding = findVariableInitializer(construction, construction.name);
30655
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
30656
+ }
30657
+ if (!construction) return null;
30658
+ const formatterName = getIntlFormatterName(construction);
30659
+ if (!formatterName) return null;
30660
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
30661
+ return {
30662
+ node: call,
30663
+ display: `Intl.${formatterName}().${callee.property.name}()`
30664
+ };
30665
+ };
30666
+ const isDeterministicInputDateConstruction = (expression) => {
30667
+ if (!expression) return false;
30668
+ const unwrapped = stripParenExpression(expression);
30669
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
30670
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
30671
+ return (unwrapped.arguments?.length ?? 0) > 0;
30672
+ };
30673
+ const matchDateDefaultStringification = (node) => {
30674
+ if (isNodeOfType(node, "CallExpression")) {
30675
+ const callee = node.callee;
30676
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
30677
+ node,
30678
+ display: "Date.prototype.toString()"
30679
+ };
30680
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
30681
+ node,
30682
+ display: "String(new Date(…))"
30683
+ };
30684
+ return null;
30685
+ }
30686
+ if (isNodeOfType(node, "TemplateLiteral")) {
30687
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
30688
+ node: expression,
30689
+ display: "`${new Date(…)}`"
30690
+ };
30691
+ }
30692
+ return null;
30693
+ };
30694
+ const findRenderPhaseComponentOrHook = (node) => {
30695
+ let functionNode = findEnclosingFunction(node);
30696
+ while (functionNode) {
30697
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30698
+ if (!executesDuringRender(functionNode)) return null;
30699
+ functionNode = findEnclosingFunction(functionNode);
30700
+ }
30701
+ return null;
30702
+ };
30703
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30704
+ if (fileHasUseClientDirective) return true;
30705
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30706
+ if (displayName && isReactHookName(displayName)) return true;
30707
+ let callsHook = false;
30708
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30709
+ if (callsHook) return false;
30710
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30711
+ callsHook = true;
30712
+ return false;
30713
+ }
30714
+ });
30715
+ return callsHook;
30716
+ };
30717
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30718
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30719
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30720
+ const ancestors = /* @__PURE__ */ new Set();
30721
+ let cursor = node;
30722
+ while (cursor) {
30723
+ ancestors.add(cursor);
30724
+ cursor = cursor.parent ?? null;
30725
+ }
30726
+ for (const statement of body.body ?? []) {
30727
+ if (ancestors.has(statement)) return false;
30728
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30729
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30730
+ let returnsEarly = false;
30731
+ walkAst(statement.consequent, (child) => {
30732
+ if (isFunctionLike$1(child)) return false;
30733
+ if (isNodeOfType(child, "ReturnStatement")) {
30734
+ returnsEarly = true;
30735
+ return false;
30736
+ }
30737
+ });
30738
+ if (returnsEarly) return true;
30739
+ }
30740
+ return false;
30741
+ };
30742
+ const findEnclosingJsxOpeningElement = (node) => {
30743
+ let cursor = node.parent;
30744
+ while (cursor) {
30745
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30746
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30747
+ cursor = cursor.parent ?? null;
30748
+ }
30749
+ return null;
30750
+ };
30751
+ const noLocaleFormatInRender = defineRule({
30752
+ id: "no-locale-format-in-render",
30753
+ title: "Locale/timezone formatting during render",
30754
+ severity: "warn",
30755
+ category: "Correctness",
30756
+ disabledWhen: [
30757
+ "vite",
30758
+ "cra",
30759
+ "expo",
30760
+ "react-native",
30761
+ "unknown"
30762
+ ],
30763
+ recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
30764
+ create: (context) => {
30765
+ if (isTestlikeFilename(context.filename)) return {};
30766
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30767
+ let fileHasUseClientDirective = false;
30768
+ let fileIsEmailTemplate = false;
30769
+ const reportedNodes = /* @__PURE__ */ new Set();
30770
+ const reportIfRenderPhase = (match) => {
30771
+ if (reportedNodes.has(match.node)) return;
30772
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30773
+ if (!componentOrHookNode) return;
30774
+ if (fileIsEmailTemplate) return;
30775
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30776
+ if (isInsideClientOnlyGuard(match.node)) return;
30777
+ if (isGatedByFalsyInitialState(match.node)) return;
30778
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30779
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30780
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30781
+ reportedNodes.add(match.node);
30782
+ context.report({
30783
+ node: match.node,
30784
+ message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30785
+ });
30786
+ };
30787
+ return {
30788
+ Program(node) {
30789
+ fileHasUseClientDirective = hasDirective(node, "use client");
30790
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30791
+ },
30792
+ CallExpression(node) {
30793
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30794
+ if (match) reportIfRenderPhase(match);
30795
+ },
30796
+ TemplateLiteral(node) {
30797
+ const match = matchDateDefaultStringification(node);
30798
+ if (match) reportIfRenderPhase(match);
30799
+ },
30800
+ JSXExpressionContainer(node) {
30801
+ const expression = stripParenExpression(node.expression);
30802
+ if (!isNodeOfType(expression, "CallExpression")) return;
30803
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30804
+ const helperName = expression.callee.name;
30805
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30806
+ if (!componentOrHookNode) return;
30807
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30808
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30809
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30810
+ walkAst(helperNode.body ?? helperNode, (child) => {
30811
+ if (isFunctionLike$1(child)) return false;
30812
+ if (!isNodeOfType(child, "CallExpression")) return;
30813
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30814
+ if (!match || reportedNodes.has(match.node)) return;
30815
+ if (fileIsEmailTemplate) return;
30816
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30817
+ if (isInsideClientOnlyGuard(node)) return;
30818
+ if (isGatedByFalsyInitialState(node)) return;
30819
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30820
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30821
+ reportedNodes.add(match.node);
30822
+ context.report({
30823
+ node: match.node,
30824
+ message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30825
+ });
30826
+ });
30827
+ }
30828
+ };
30829
+ }
30830
+ });
30831
+ //#endregion
29336
30832
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29337
30833
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29338
30834
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30146,7 +31642,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30146
31642
  "reverse",
30147
31643
  "sort"
30148
31644
  ]);
30149
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30150
31645
  const OBJECT_MUTATION_METHODS = new Set([
30151
31646
  "assign",
30152
31647
  "defineProperties",
@@ -30220,7 +31715,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30220
31715
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30221
31716
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30222
31717
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30223
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30224
31718
  }
30225
31719
  }
30226
31720
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30259,6 +31753,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30259
31753
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30260
31754
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30261
31755
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
31756
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30262
31757
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30263
31758
  });
30264
31759
  return mutations;
@@ -30491,7 +31986,7 @@ const noNestedComponentDefinition = defineRule({
30491
31986
  id: "no-nested-component-definition",
30492
31987
  title: "Component defined inside another component",
30493
31988
  tags: ["test-noise", "react-jsx-only"],
30494
- severity: "error",
31989
+ severity: "warn",
30495
31990
  category: "Correctness",
30496
31991
  recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
30497
31992
  create: (context) => {
@@ -31446,12 +32941,12 @@ const noPassDataToParent = defineRule({
31446
32941
  if (calleeNode === identifier) {
31447
32942
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31448
32943
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31449
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32944
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31450
32945
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31451
32946
  if (isCustomHookParameter(ref)) continue;
31452
32947
  } else continue;
31453
32948
  const methodName = getCallMethodName(calleeNode);
31454
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32949
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31455
32950
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31456
32951
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31457
32952
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31642,7 +33137,7 @@ const noPassLiveStateToParent = defineRule({
31642
33137
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31643
33138
  const calleeNode = callExpr.callee;
31644
33139
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31645
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
33140
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31646
33141
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31647
33142
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31648
33143
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -31970,40 +33465,6 @@ const noPreventDefault = defineRule({
31970
33465
  }
31971
33466
  });
31972
33467
  //#endregion
31973
- //#region src/plugin/utils/is-result-discarded-call.ts
31974
- const isResultDiscardedCall = (callExpression) => {
31975
- let node = callExpression;
31976
- let parent = node.parent;
31977
- while (parent) {
31978
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
31979
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31980
- if (isNodeOfType(parent, "ChainExpression")) {
31981
- node = parent;
31982
- parent = node.parent;
31983
- continue;
31984
- }
31985
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31986
- node = parent;
31987
- parent = node.parent;
31988
- continue;
31989
- }
31990
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31991
- node = parent;
31992
- parent = node.parent;
31993
- continue;
31994
- }
31995
- if (isNodeOfType(parent, "SequenceExpression")) {
31996
- const expressions = parent.expressions ?? [];
31997
- if (expressions[expressions.length - 1] !== node) return true;
31998
- node = parent;
31999
- parent = node.parent;
32000
- continue;
32001
- }
32002
- return false;
32003
- }
32004
- return false;
32005
- };
32006
- //#endregion
32007
33468
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
32008
33469
  const isRefLatchGuardedEffect = (callbackBody) => {
32009
33470
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32210,7 +33671,7 @@ const isAlwaysFreshExpression = (expression) => {
32210
33671
  return `${callee.name}()`;
32211
33672
  }
32212
33673
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32213
- const receiver = callee.object;
33674
+ const receiver = stripParenExpression(callee.object);
32214
33675
  const property = callee.property;
32215
33676
  if (!isNodeOfType(property, "Identifier")) return null;
32216
33677
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32331,9 +33792,11 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32331
33792
  if (typeof sourceValue !== "string") return;
32332
33793
  if (handleExtraSource?.(node, context)) return;
32333
33794
  if (sourceValue !== source) return;
33795
+ if (isTypeOnlyImport(node)) return;
32334
33796
  for (const specifier of node.specifiers ?? []) {
32335
33797
  if (isNodeOfType(specifier, "ImportSpecifier")) {
32336
- const importedName = getImportedName$1(specifier);
33798
+ if (specifier.importKind === "type") continue;
33799
+ const importedName = getImportedName(specifier);
32337
33800
  if (!importedName) continue;
32338
33801
  const message = messages.get(importedName);
32339
33802
  if (message) context.report({
@@ -32351,8 +33814,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32351
33814
  MemberExpression(node) {
32352
33815
  if (namespaceBindings.size === 0) return;
32353
33816
  if (node.computed) return;
32354
- if (!isNodeOfType(node.object, "Identifier")) return;
32355
- if (!namespaceBindings.has(node.object.name)) return;
33817
+ const receiver = stripParenExpression(node.object);
33818
+ if (!isNodeOfType(receiver, "Identifier")) return;
33819
+ if (!namespaceBindings.has(receiver.name)) return;
32356
33820
  if (!isNodeOfType(node.property, "Identifier")) return;
32357
33821
  const message = messages.get(node.property.name);
32358
33822
  if (message) context.report({
@@ -32385,7 +33849,7 @@ const buildTestUtilsMessage = (importedName) => {
32385
33849
  const reportTestUtilsImports = (node, context) => {
32386
33850
  for (const specifier of node.specifiers ?? []) {
32387
33851
  if (isNodeOfType(specifier, "ImportSpecifier")) {
32388
- const importedName = getImportedName$1(specifier) ?? "default";
33852
+ const importedName = getImportedName(specifier) ?? "default";
32389
33853
  context.report({
32390
33854
  node: specifier,
32391
33855
  message: buildTestUtilsMessage(importedName)
@@ -32417,7 +33881,7 @@ const noReactDomDeprecatedApis = defineRule({
32417
33881
  });
32418
33882
  //#endregion
32419
33883
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32420
- const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
33884
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
32421
33885
  const isVendoredShadcnUiFilename = (rawFilename) => {
32422
33886
  if (!rawFilename) return false;
32423
33887
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32455,7 +33919,7 @@ const noReact19DeprecatedApis = defineRule({
32455
33919
  requires: ["react:19"],
32456
33920
  tags: ["test-noise", "migration-hint"],
32457
33921
  severity: "warn",
32458
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
33922
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32459
33923
  create: (context) => {
32460
33924
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32461
33925
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32779,34 +34243,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32779
34243
  });
32780
34244
  //#endregion
32781
34245
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32782
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32783
- if (!symbol || visitedSymbols.has(symbol)) return false;
32784
- visitedSymbols.add(symbol);
32785
- if (isComponentParameterSymbol(symbol)) return true;
32786
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32787
- const source = symbol.initializer;
32788
- if (!source) return false;
32789
- return initializerRootsInProps(source, scopes, visitedSymbols);
32790
- };
32791
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32792
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32793
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32794
- return rootsInProps(node, scopes, visitedSymbols);
32795
- };
32796
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32797
- let current = node;
32798
- while (isNodeOfType(current, "MemberExpression")) {
32799
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32800
- current = current.object;
32801
- }
32802
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32803
- return false;
32804
- };
32805
34246
  const isInsideComponentContext = (node) => {
32806
34247
  let cursor = node.parent;
32807
34248
  while (cursor) {
32808
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32809
34249
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
34250
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32810
34251
  cursor = cursor.parent ?? null;
32811
34252
  }
32812
34253
  return false;
@@ -32816,24 +34257,28 @@ const functionBodyOf = (node) => {
32816
34257
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32817
34258
  return null;
32818
34259
  };
34260
+ const isHookCallee = (callee) => {
34261
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
34262
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
34263
+ return false;
34264
+ };
32819
34265
  const containsHookCall = (body) => {
32820
34266
  let found = false;
32821
34267
  walkAst(body, (child) => {
32822
- if (found) return;
34268
+ if (found) return false;
34269
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32823
34270
  if (!isNodeOfType(child, "CallExpression")) return;
32824
- const name = getCalleeName$2(child);
32825
- if (name && isReactHookName(name)) found = true;
34271
+ if (isHookCallee(child.callee)) found = true;
32826
34272
  });
32827
34273
  return found;
32828
34274
  };
32829
- const isModuleScopeHookFreeHelper = (symbol) => {
34275
+ const isHookCallingRenderHelper = (symbol) => {
32830
34276
  if (!symbol) return false;
32831
34277
  const declaration = symbol.declarationNode;
32832
34278
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32833
34279
  const body = functionBodyOf(declaration);
32834
34280
  if (!body) return false;
32835
- if (findEnclosingFunction(declaration) !== null) return false;
32836
- return !containsHookCall(body);
34281
+ return containsHookCall(body);
32837
34282
  };
32838
34283
  const noRenderInRender = defineRule({
32839
34284
  id: "no-render-in-render",
@@ -32842,20 +34287,13 @@ const noRenderInRender = defineRule({
32842
34287
  tags: ["test-noise"],
32843
34288
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32844
34289
  create: (context) => ({ JSXExpressionContainer(node) {
32845
- const expression = node.expression;
34290
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32846
34291
  if (!isNodeOfType(expression, "CallExpression")) return;
32847
- let calleeName = null;
32848
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32849
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32850
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
34292
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
34293
+ const calleeName = expression.callee.name;
34294
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32851
34295
  if (!isInsideComponentContext(node)) return;
32852
- if (isNodeOfType(expression.callee, "Identifier")) {
32853
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
32854
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32855
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32856
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32857
- if (rootsInProps(expression.callee.object, context.scopes)) return;
32858
- }
34296
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32859
34297
  context.report({
32860
34298
  node: expression,
32861
34299
  message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
@@ -32916,13 +34354,7 @@ const noRenderPropChildren = defineRule({
32916
34354
  //#endregion
32917
34355
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32918
34356
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
32919
- const isReactDomRenderCall = (node) => {
32920
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32921
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32922
- if (node.callee.object.name !== "ReactDOM") return false;
32923
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32924
- return node.callee.property.name === "render";
32925
- };
34357
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32926
34358
  const isUsedAsReturnValue = (parent) => {
32927
34359
  if (!parent) return false;
32928
34360
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33758,7 +35190,7 @@ const noSetState = defineRule({
33758
35190
  category: "Architecture",
33759
35191
  create: (context) => ({ CallExpression(node) {
33760
35192
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33761
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
35193
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33762
35194
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33763
35195
  if (!getParentComponent(node)) return;
33764
35196
  context.report({
@@ -33924,6 +35356,204 @@ const noSideTabBorder = defineRule({
33924
35356
  })
33925
35357
  });
33926
35358
  //#endregion
35359
+ //#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
35360
+ const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
35361
+ "window",
35362
+ "globalThis",
35363
+ "self"
35364
+ ]);
35365
+ const NULLISH_COMPARISON_OPERATORS = new Set([
35366
+ "==",
35367
+ "===",
35368
+ "!=",
35369
+ "!=="
35370
+ ]);
35371
+ const getGlobalTimerCalleeName = (node) => {
35372
+ if (!isNodeOfType(node, "CallExpression")) return null;
35373
+ const callee = stripParenExpression(node.callee);
35374
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
35375
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
35376
+ const receiver = stripParenExpression(callee.object);
35377
+ if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
35378
+ }
35379
+ return null;
35380
+ };
35381
+ const getRefCurrentReceiverName = (node) => {
35382
+ if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
35383
+ if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
35384
+ const receiver = stripParenExpression(node.object);
35385
+ return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
35386
+ };
35387
+ const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
35388
+ const parseClearCallOnTimerRef = (node) => {
35389
+ const clearCalleeName = getGlobalTimerCalleeName(node);
35390
+ if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
35391
+ if (!isNodeOfType(node, "CallExpression")) return null;
35392
+ const clearedArgument = node.arguments?.[0];
35393
+ if (!clearedArgument) return null;
35394
+ const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
35395
+ return refName === null ? null : {
35396
+ clearCalleeName,
35397
+ refName
35398
+ };
35399
+ };
35400
+ const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
35401
+ const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
35402
+ const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
35403
+ const isClearGuardIfIdiom = (ifStatement, refName) => {
35404
+ if (ifStatement.alternate) return false;
35405
+ const consequent = ifStatement.consequent;
35406
+ return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
35407
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
35408
+ const expression = stripParenExpression(statement.expression);
35409
+ if (isClearCallOnRef(expression, refName)) return true;
35410
+ return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
35411
+ });
35412
+ };
35413
+ const climbBooleanProjection = (refCurrentMember) => {
35414
+ let cursor = refCurrentMember;
35415
+ const logicalAncestors = [];
35416
+ let didPassBooleanProjection = false;
35417
+ while (cursor.parent) {
35418
+ const parent = cursor.parent;
35419
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
35420
+ cursor = parent;
35421
+ continue;
35422
+ }
35423
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
35424
+ didPassBooleanProjection = true;
35425
+ cursor = parent;
35426
+ continue;
35427
+ }
35428
+ if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
35429
+ didPassBooleanProjection = true;
35430
+ cursor = parent;
35431
+ continue;
35432
+ }
35433
+ if (isNodeOfType(parent, "LogicalExpression")) {
35434
+ logicalAncestors.push(parent);
35435
+ cursor = parent;
35436
+ continue;
35437
+ }
35438
+ break;
35439
+ }
35440
+ return {
35441
+ conditionRoot: cursor,
35442
+ logicalAncestors,
35443
+ didPassBooleanProjection
35444
+ };
35445
+ };
35446
+ const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
35447
+ const isPendingSignalRead = (refCurrentMember, refName) => {
35448
+ const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
35449
+ const conditionParent = conditionRoot.parent;
35450
+ if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
35451
+ if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
35452
+ if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
35453
+ return didPassBooleanProjection;
35454
+ };
35455
+ const collectTimerRefUsageFacts = (ownerScope, refName) => {
35456
+ const facts = {
35457
+ holdsScheduledTimerId: false,
35458
+ hasPendingSignalRead: false
35459
+ };
35460
+ walkAst(ownerScope, (child) => {
35461
+ if (isAssignmentToRefCurrent(child, refName)) {
35462
+ const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
35463
+ if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
35464
+ return;
35465
+ }
35466
+ if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
35467
+ });
35468
+ return facts;
35469
+ };
35470
+ const isEffectCallbackFunction = (functionNode) => {
35471
+ const parent = functionNode.parent;
35472
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
35473
+ return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
35474
+ };
35475
+ const doesEffectCallbackReturnName = (effectCallback, name) => {
35476
+ if (!isFunctionLike$1(effectCallback)) return false;
35477
+ let isReturnedByName = false;
35478
+ walkInsideStatementBlocks(effectCallback.body, (child) => {
35479
+ if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
35480
+ const returned = stripParenExpression(child.argument);
35481
+ if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
35482
+ });
35483
+ return isReturnedByName;
35484
+ };
35485
+ const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
35486
+ if (!isFunctionLike$1(effectCallback)) return false;
35487
+ if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
35488
+ if (effectCallback.body === functionNode) return true;
35489
+ const cleanupBindingName = getFunctionBindingName$1(functionNode);
35490
+ return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
35491
+ };
35492
+ const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
35493
+ const cleanupBindingName = getFunctionBindingName$1(functionNode);
35494
+ if (cleanupBindingName === null) return false;
35495
+ let isReturnedFromEffect = false;
35496
+ walkAst(ownerScope, (child) => {
35497
+ if (isReturnedFromEffect) return false;
35498
+ if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
35499
+ const effectCallback = getEffectCallback(child);
35500
+ if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
35501
+ });
35502
+ return isReturnedFromEffect;
35503
+ };
35504
+ const isInsideEffectCleanupReturn = (node, ownerScope) => {
35505
+ let functionNode = findEnclosingFunction(node);
35506
+ while (functionNode) {
35507
+ const outerFunction = findEnclosingFunction(functionNode);
35508
+ if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
35509
+ if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
35510
+ functionNode = outerFunction;
35511
+ }
35512
+ return false;
35513
+ };
35514
+ const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
35515
+ const enclosingFunction = findEnclosingFunction(clearCall);
35516
+ if (!isFunctionLike$1(enclosingFunction)) return false;
35517
+ if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
35518
+ const clearStart = getRangeStart(clearCall);
35519
+ if (clearStart === null) return false;
35520
+ let didFindLaterReassignment = false;
35521
+ walkInsideStatementBlocks(enclosingFunction.body, (child) => {
35522
+ if (didFindLaterReassignment) return;
35523
+ if (!isAssignmentToRefCurrent(child, refName)) return;
35524
+ const assignmentStart = getRangeStart(child);
35525
+ if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
35526
+ });
35527
+ return didFindLaterReassignment;
35528
+ };
35529
+ const isShadowedTimerGlobal = (clearCall) => {
35530
+ const callee = stripParenExpression(clearCall.callee);
35531
+ if (!isNodeOfType(callee, "Identifier")) return false;
35532
+ return findVariableInitializer(callee, callee.name) !== null;
35533
+ };
35534
+ const noStaleTimerRef = defineRule({
35535
+ id: "no-stale-timer-ref",
35536
+ title: "Cleared timer ref keeps the stale id",
35537
+ severity: "warn",
35538
+ recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
35539
+ create: (context) => ({ CallExpression(node) {
35540
+ const clearCall = parseClearCallOnTimerRef(node);
35541
+ if (!clearCall) return;
35542
+ if (isShadowedTimerGlobal(node)) return;
35543
+ const { clearCalleeName, refName } = clearCall;
35544
+ const refBinding = findVariableInitializer(node, refName);
35545
+ if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
35546
+ const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
35547
+ if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
35548
+ if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
35549
+ if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
35550
+ context.report({
35551
+ node,
35552
+ message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
35553
+ });
35554
+ } })
35555
+ });
35556
+ //#endregion
33927
35557
  //#region src/plugin/utils/is-abstract-role.ts
33928
35558
  const isAbstractRole = (openingElement, settings) => {
33929
35559
  const elementType = getElementType(openingElement, settings);
@@ -36204,6 +37834,36 @@ const isTriviallyCheapExpression = (node) => {
36204
37834
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36205
37835
  return true;
36206
37836
  };
37837
+ const isTrivialContainerLiteral = (node) => {
37838
+ if (!node) return false;
37839
+ const innerExpression = stripParenExpression(node);
37840
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
37841
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
37842
+ return false;
37843
+ };
37844
+ const isNonEscapingRead = (identifier) => {
37845
+ const readRoot = findTransparentExpressionRoot(identifier);
37846
+ const memberNode = readRoot.parent;
37847
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
37848
+ const memberUse = findTransparentExpressionRoot(memberNode);
37849
+ const memberUseParent = memberUse.parent;
37850
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
37851
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
37852
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
37853
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
37854
+ };
37855
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
37856
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
37857
+ const parentNode = memoUsageRoot.parent;
37858
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
37859
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
37860
+ const bindingTarget = parentNode.id;
37861
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
37862
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
37863
+ const symbol = scopes.symbolFor(bindingTarget);
37864
+ if (!symbol) return false;
37865
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
37866
+ };
36207
37867
  const noUsememoSimpleExpression = defineRule({
36208
37868
  id: "no-usememo-simple-expression",
36209
37869
  title: "useMemo on a cheap value",
@@ -36225,9 +37885,17 @@ const noUsememoSimpleExpression = defineRule({
36225
37885
  let returnExpression = null;
36226
37886
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36227
37887
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36228
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
37888
+ if (!returnExpression) return;
37889
+ if (isTriviallyCheapExpression(returnExpression)) {
37890
+ context.report({
37891
+ node,
37892
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37893
+ });
37894
+ return;
37895
+ }
37896
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36229
37897
  node,
36230
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37898
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36231
37899
  });
36232
37900
  } })
36233
37901
  });
@@ -36333,7 +38001,7 @@ const noWillUpdateSetState = defineRule({
36333
38001
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36334
38002
  return { CallExpression(node) {
36335
38003
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36336
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
38004
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36337
38005
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36338
38006
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36339
38007
  context.report({
@@ -36650,6 +38318,10 @@ const isRouteFactoryCall = (expression) => {
36650
38318
  }
36651
38319
  return false;
36652
38320
  };
38321
+ const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
38322
+ const expression = skipTsExpression(argument);
38323
+ return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
38324
+ });
36653
38325
  const isReactHocName = (name, state) => state.customHocs.has(name);
36654
38326
  const isHocCallee = (callee, state) => {
36655
38327
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -36689,7 +38361,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
36689
38361
  continue;
36690
38362
  }
36691
38363
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36692
- if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
38364
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
36693
38365
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36694
38366
  }
36695
38367
  return false;
@@ -36797,18 +38469,22 @@ const onlyExportComponents = defineRule({
36797
38469
  customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36798
38470
  allowExportNames: new Set(settings.allowExportNames),
36799
38471
  allowConstantExport: settings.allowConstantExport,
36800
- localComponentNames
38472
+ localComponentNames,
38473
+ scopes: context.scopes
36801
38474
  };
36802
38475
  for (const child of componentCandidates) {
36803
38476
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36804
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
38477
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
36805
38478
  }
36806
38479
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
36807
38480
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36808
38481
  }
36809
38482
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36810
38483
  const initializer = child.init;
36811
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
38484
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
38485
+ const expression = initializer ? skipTsExpression(initializer) : null;
38486
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
38487
+ }
36812
38488
  }
36813
38489
  }
36814
38490
  const exports = [];
@@ -36878,6 +38554,10 @@ const onlyExportComponents = defineRule({
36878
38554
  return false;
36879
38555
  })();
36880
38556
  if (isHoc && firstArgIsValid) hasReactExport = true;
38557
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
38558
+ kind: "non-component",
38559
+ reportNode: stripped
38560
+ });
36881
38561
  else context.report({
36882
38562
  node: stripped,
36883
38563
  message: ANONYMOUS_MESSAGE
@@ -37768,8 +39448,8 @@ const preferHtmlDialog = defineRule({
37768
39448
  if (!HTML_TAGS.has(tagName)) return;
37769
39449
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37770
39450
  if (roleAttribute) {
37771
- const roleValue = getJsxPropStringValue(roleAttribute);
37772
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
39451
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
39452
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37773
39453
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37774
39454
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37775
39455
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -38473,11 +40153,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38473
40153
  }
38474
40154
  return null;
38475
40155
  };
40156
+ const isTrivialLiteralExpression = (expression) => {
40157
+ if (isNodeOfType(expression, "Literal")) return true;
40158
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
40159
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
40160
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
40161
+ return false;
40162
+ };
38476
40163
  const getSingleSetterCallFromHandler = (handler) => {
38477
40164
  const handlerStatements = getCallbackStatements(handler);
38478
40165
  if (handlerStatements.length !== 1) return null;
38479
40166
  const onlyStatement = handlerStatements[0];
38480
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
40167
+ let expression = onlyStatement;
40168
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
40169
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
40170
+ if (!expression) return null;
40171
+ expression = stripParenExpression(expression);
38481
40172
  if (!isNodeOfType(expression, "CallExpression")) return null;
38482
40173
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38483
40174
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38487,6 +40178,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38487
40178
  setterArgument: expression.arguments[0]
38488
40179
  };
38489
40180
  };
40181
+ const isListenerCollectionInitializer = (init) => {
40182
+ if (!init) return false;
40183
+ if (isNodeOfType(init, "ArrayExpression")) return true;
40184
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
40185
+ };
40186
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
40187
+ if (!isFunctionLike$1(functionNode)) return false;
40188
+ const firstParam = functionNode.params?.[0];
40189
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
40190
+ const listenerParamName = firstParam.name;
40191
+ let registersListener = false;
40192
+ walkAst(functionNode.body, (child) => {
40193
+ if (registersListener) return false;
40194
+ if (!isNodeOfType(child, "CallExpression")) return;
40195
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
40196
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
40197
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
40198
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
40199
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
40200
+ const registeredArgument = child.arguments?.[0];
40201
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
40202
+ });
40203
+ return registersListener;
40204
+ };
40205
+ const buildModuleScopeStoreIndex = (programRoot) => {
40206
+ const mutableBindingNames = /* @__PURE__ */ new Set();
40207
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
40208
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
40209
+ if (!isNodeOfType(programRoot, "Program")) return {
40210
+ mutableBindingNames,
40211
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
40212
+ };
40213
+ for (const statement of programRoot.body ?? []) {
40214
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
40215
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
40216
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
40217
+ continue;
40218
+ }
40219
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
40220
+ for (const declarator of unwrapped.declarations ?? []) {
40221
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
40222
+ const init = declarator.init ?? null;
40223
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
40224
+ mutableBindingNames.add(declarator.id.name);
40225
+ continue;
40226
+ }
40227
+ if (isListenerCollectionInitializer(init)) {
40228
+ listenerCollectionNames.add(declarator.id.name);
40229
+ continue;
40230
+ }
40231
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
40232
+ }
40233
+ }
40234
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
40235
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
40236
+ return {
40237
+ mutableBindingNames,
40238
+ subscribeFunctionNames
40239
+ };
40240
+ };
40241
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
40242
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
40243
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
40244
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
40245
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
40246
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
40247
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
40248
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
40249
+ return initialArgument.name;
40250
+ };
40251
+ const argumentForwardsSetter = (argument, setterName) => {
40252
+ if (!argument) return false;
40253
+ const unwrapped = stripParenExpression(argument);
40254
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
40255
+ if (!isFunctionLike$1(unwrapped)) return false;
40256
+ let callsSetter = false;
40257
+ walkAst(unwrapped.body, (child) => {
40258
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
40259
+ callsSetter = true;
40260
+ return false;
40261
+ }
40262
+ });
40263
+ return callsSetter;
40264
+ };
40265
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
40266
+ let matchedCall = null;
40267
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
40268
+ if (matchedCall) return false;
40269
+ if (!isNodeOfType(child, "CallExpression")) return;
40270
+ if (!isNodeOfType(child.callee, "Identifier")) return;
40271
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
40272
+ const binding = findVariableInitializer(child.callee, child.callee.name);
40273
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
40274
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
40275
+ matchedCall = child;
40276
+ return false;
40277
+ }
40278
+ });
40279
+ return matchedCall;
40280
+ };
38490
40281
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38491
40282
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38492
40283
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38503,6 +40294,16 @@ const preferUseSyncExternalStore = defineRule({
38503
40294
  severity: "warn",
38504
40295
  recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
38505
40296
  create: (context) => {
40297
+ let cachedStoreIndex = null;
40298
+ const storeIndexFor = (node) => {
40299
+ if (cachedStoreIndex) return cachedStoreIndex;
40300
+ const programRoot = findProgramRoot(node);
40301
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
40302
+ mutableBindingNames: /* @__PURE__ */ new Set(),
40303
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
40304
+ };
40305
+ return cachedStoreIndex;
40306
+ };
38506
40307
  const checkComponent = (componentBody) => {
38507
40308
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38508
40309
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38540,6 +40341,7 @@ const preferUseSyncExternalStore = defineRule({
38540
40341
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38541
40342
  if (!useStateInitializer) continue;
38542
40343
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
40344
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38543
40345
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38544
40346
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38545
40347
  context.report({
@@ -38547,14 +40349,47 @@ const preferUseSyncExternalStore = defineRule({
38547
40349
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38548
40350
  });
38549
40351
  }
40352
+ checkModuleStoreShape(componentBody, useStateBindings);
40353
+ };
40354
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
40355
+ const storeIndex = storeIndexFor(componentBody);
40356
+ if (storeIndex.mutableBindingNames.size === 0) return;
40357
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
40358
+ const snapshotBindings = useStateBindings.map((binding) => ({
40359
+ binding,
40360
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
40361
+ })).filter((candidate) => candidate.storeName !== null);
40362
+ if (snapshotBindings.length === 0) return;
40363
+ const reportedDeclarators = /* @__PURE__ */ new Set();
40364
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
40365
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
40366
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
40367
+ const depsNode = effectCall.arguments[1];
40368
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
40369
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
40370
+ const callback = getEffectCallback(effectCall);
40371
+ if (!callback || !isFunctionLike$1(callback)) continue;
40372
+ for (const { binding, storeName } of snapshotBindings) {
40373
+ if (reportedDeclarators.has(binding.declarator)) continue;
40374
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
40375
+ reportedDeclarators.add(binding.declarator);
40376
+ context.report({
40377
+ node: binding.declarator,
40378
+ message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
40379
+ });
40380
+ }
40381
+ }
38550
40382
  };
38551
40383
  return {
38552
40384
  FunctionDeclaration(node) {
38553
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
40385
+ const functionName = node.id?.name;
40386
+ if (!functionName) return;
40387
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38554
40388
  checkComponent(node.body);
38555
40389
  },
38556
40390
  VariableDeclarator(node) {
38557
- if (!isComponentAssignment(node)) return;
40391
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
40392
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38558
40393
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38559
40394
  checkComponent(node.init.body);
38560
40395
  }
@@ -39164,7 +40999,7 @@ const queryNoVoidQueryFn = defineRule({
39164
40999
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39165
41000
  const body = queryFnValue.body;
39166
41001
  if (!isNodeOfType(body, "BlockStatement")) return;
39167
- if ((body.body ?? []).length === 0) context.report({
41002
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39168
41003
  node: queryFnProperty,
39169
41004
  message: "This empty queryFn caches undefined, so the component never gets data."
39170
41005
  });
@@ -39671,17 +41506,6 @@ const renderingHoistJsx = defineRule({
39671
41506
  }
39672
41507
  });
39673
41508
  //#endregion
39674
- //#region src/plugin/utils/find-declarator-for-binding.ts
39675
- const findDeclaratorForBinding = (bindingIdentifier) => {
39676
- let ancestor = bindingIdentifier.parent;
39677
- while (ancestor) {
39678
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39679
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39680
- ancestor = ancestor.parent ?? null;
39681
- }
39682
- return null;
39683
- };
39684
- //#endregion
39685
41509
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39686
41510
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39687
41511
  {
@@ -39690,19 +41514,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39690
41514
  },
39691
41515
  {
39692
41516
  display: "Date.now()",
39693
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
41517
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39694
41518
  },
39695
41519
  {
39696
41520
  display: "Math.random()",
39697
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
41521
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39698
41522
  },
39699
41523
  {
39700
41524
  display: "performance.now()",
39701
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
41525
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39702
41526
  },
39703
41527
  {
39704
41528
  display: "crypto.randomUUID()",
39705
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
41529
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39706
41530
  }
39707
41531
  ];
39708
41532
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39714,54 +41538,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39714
41538
  }
39715
41539
  return null;
39716
41540
  };
39717
- const executesDuringRender = (functionNode) => {
39718
- const parent = functionNode.parent;
39719
- if (!isNodeOfType(parent, "CallExpression")) return false;
39720
- if (parent.callee === functionNode) return true;
39721
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39722
- };
39723
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39724
- const referencesClientOnlyFlag = (expression) => {
39725
- const unwrapped = stripParenExpression(expression);
39726
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39727
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39728
- const property = unwrapped.property;
39729
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39730
- }
39731
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39732
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39733
- return false;
39734
- };
39735
- const isFalsyLiteral = (node) => {
39736
- if (!node) return true;
39737
- if (isNodeOfType(node, "Literal")) return !node.value;
39738
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39739
- };
39740
- const isFalsyInitialStateBinding = (identifier) => {
39741
- if (!isNodeOfType(identifier, "Identifier")) return false;
39742
- const binding = findVariableInitializer(identifier, identifier.name);
39743
- if (!binding) return false;
39744
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39745
- if (!declarator?.init) return false;
39746
- const init = stripParenExpression(declarator.init);
39747
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39748
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39749
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39750
- return isFalsyLiteral(init.arguments?.[0]);
39751
- };
39752
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39753
- const isGatedByFalsyInitialState = (node) => {
39754
- let cursor = node;
39755
- let parent = node.parent;
39756
- while (parent) {
39757
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39758
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39759
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39760
- cursor = parent;
39761
- parent = parent.parent ?? null;
39762
- }
39763
- return false;
39764
- };
39765
41541
  const isYearOnlyDateRead = (dateNode) => {
39766
41542
  const member = dateNode.parent;
39767
41543
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39785,23 +41561,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39785
41561
  }
39786
41562
  return false;
39787
41563
  };
39788
- const isInsideClientOnlyGuard = (node) => {
39789
- let cursor = node;
39790
- let parent = node.parent;
39791
- while (parent) {
39792
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39793
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39794
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39795
- cursor = parent;
39796
- parent = parent.parent ?? null;
39797
- }
39798
- return false;
39799
- };
39800
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
39801
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39802
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39803
- return false;
39804
- };
39805
41564
  const renderingHydrationMismatchTime = defineRule({
39806
41565
  id: "rendering-hydration-mismatch-time",
39807
41566
  title: "Time or random value in JSX",
@@ -39849,6 +41608,35 @@ const renderingHydrationMismatchTime = defineRule({
39849
41608
  }
39850
41609
  });
39851
41610
  //#endregion
41611
+ //#region src/plugin/utils/contains-locale-environment-read.ts
41612
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
41613
+ "toLocaleString",
41614
+ "toLocaleDateString",
41615
+ "toLocaleTimeString",
41616
+ "getTimezoneOffset"
41617
+ ]);
41618
+ const containsLocaleEnvironmentRead = (expression) => {
41619
+ let readsLocaleEnvironment = false;
41620
+ walkAst(expression, (child) => {
41621
+ if (readsLocaleEnvironment) return false;
41622
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
41623
+ if (child.object.name === "Intl") {
41624
+ readsLocaleEnvironment = true;
41625
+ return false;
41626
+ }
41627
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
41628
+ readsLocaleEnvironment = true;
41629
+ return false;
41630
+ }
41631
+ }
41632
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
41633
+ readsLocaleEnvironment = true;
41634
+ return false;
41635
+ }
41636
+ });
41637
+ return readsLocaleEnvironment;
41638
+ };
41639
+ //#endregion
39852
41640
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39853
41641
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39854
41642
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39914,14 +41702,15 @@ const renderingHydrationNoFlicker = defineRule({
39914
41702
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39915
41703
  const callback = getEffectCallback(node);
39916
41704
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
39917
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39918
- if (!bodyStatements || bodyStatements.length !== 1) return;
41705
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
41706
+ if (bodyStatements.length !== 1) return;
39919
41707
  const soleStatement = bodyStatements[0];
39920
41708
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39921
41709
  const expression = soleStatement.expression;
39922
41710
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39923
41711
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39924
41712
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
41713
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39925
41714
  context.report({
39926
41715
  node,
39927
41716
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40740,6 +42529,9 @@ const rerenderFunctionalSetstate = defineRule({
40740
42529
  } })
40741
42530
  });
40742
42531
  //#endregion
42532
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
42533
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
42534
+ //#endregion
40743
42535
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40744
42536
  const rerenderLazyRefInit = defineRule({
40745
42537
  id: "rerender-lazy-ref-init",
@@ -40750,7 +42542,7 @@ const rerenderLazyRefInit = defineRule({
40750
42542
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40751
42543
  create: (context) => ({ CallExpression(node) {
40752
42544
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40753
- const initializer = node.arguments[0];
42545
+ const initializer = stripParenExpression(node.arguments[0]);
40754
42546
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40755
42547
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40756
42548
  if (!isPlainCall && !isNewCall) return;
@@ -40758,7 +42550,7 @@ const rerenderLazyRefInit = defineRule({
40758
42550
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40759
42551
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40760
42552
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40761
- if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
42553
+ if (isTrivialBuiltInConstruction(initializer)) return;
40762
42554
  if (isPlainCall && isReactHookName(calleeName)) return;
40763
42555
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40764
42556
  context.report({
@@ -40794,11 +42586,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40794
42586
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40795
42587
  const findEagerInitializerCall = (expression, depth = 0) => {
40796
42588
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
40797
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40798
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40799
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40800
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40801
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
42589
+ const innerExpression = stripParenExpression(expression);
42590
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
42591
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
42592
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
42593
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40802
42594
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40803
42595
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40804
42596
  if (spreadCall) return spreadCall;
@@ -40821,7 +42613,7 @@ const rerenderLazyStateInit = defineRule({
40821
42613
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40822
42614
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40823
42615
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40824
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
42616
+ if (isTrivialBuiltInConstruction(initializer)) return;
40825
42617
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40826
42618
  if (isReactHookName(calleeName)) return;
40827
42619
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41488,7 +43280,7 @@ const rnAnimationReactionAsDerived = defineRule({
41488
43280
  const body = reactionFn.body;
41489
43281
  let singleAssignment = null;
41490
43282
  if (isNodeOfType(body, "BlockStatement")) {
41491
- const statements = body.body ?? [];
43283
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41492
43284
  if (statements.length !== 1) return;
41493
43285
  const onlyStatement = statements[0];
41494
43286
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41562,7 +43354,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41562
43354
  "catch",
41563
43355
  "finally"
41564
43356
  ]);
41565
- const findChainRoot = (node) => {
43357
+ const findChainRoot = (wrappedNode) => {
43358
+ const node = stripParenExpression(wrappedNode);
41566
43359
  if (isNodeOfType(node, "CallExpression")) {
41567
43360
  if (isNodeOfType(node.callee, "Identifier")) return {
41568
43361
  calleeName: node.callee.name,
@@ -42116,7 +43909,7 @@ const rnNoDeprecatedModules = defineRule({
42116
43909
  for (const specifier of node.specifiers ?? []) {
42117
43910
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
42118
43911
  if (specifier.importKind === "type") continue;
42119
- const importedName = getImportedName$1(specifier);
43912
+ const importedName = getImportedName(specifier);
42120
43913
  if (!importedName) continue;
42121
43914
  if (!DEPRECATED_RN_MODULE_REPLACEMENTS.get(importedName)) continue;
42122
43915
  context.report({
@@ -42150,20 +43943,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42150
43943
  };
42151
43944
  const isReactNativeDimensionsCallee = (node, callee) => {
42152
43945
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42153
- if (isNodeOfType(callee.object, "Identifier")) {
42154
- const localName = callee.object.name;
43946
+ const receiver = stripParenExpression(callee.object);
43947
+ if (isNodeOfType(receiver, "Identifier")) {
43948
+ const localName = receiver.name;
42155
43949
  const binding = findVariableInitializer(node, localName);
42156
43950
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42157
43951
  return localName === "Dimensions";
42158
43952
  }
42159
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42160
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42161
- const rootName = getRootIdentifierName(callee.object.object);
43953
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
43954
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
43955
+ const rootName = getRootIdentifierName(receiver.object);
42162
43956
  if (rootName === null) return false;
42163
43957
  const importBinding = getImportBindingForName(node, rootName);
42164
43958
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42165
43959
  }
42166
- if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
43960
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42167
43961
  return false;
42168
43962
  };
42169
43963
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42607,7 +44401,8 @@ const rnNoLegacyShadowStyles = defineRule({
42607
44401
  },
42608
44402
  CallExpression(node) {
42609
44403
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42610
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
44404
+ const receiver = stripParenExpression(node.callee.object);
44405
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42611
44406
  if (!isMemberProperty(node.callee, "create")) return;
42612
44407
  const stylesArgument = node.arguments?.[0];
42613
44408
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -42666,7 +44461,7 @@ const rnNoPanresponder = defineRule({
42666
44461
  for (const specifier of node.specifiers ?? []) {
42667
44462
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
42668
44463
  if (specifier.importKind === "type") continue;
42669
- if (getImportedName$1(specifier) !== "PanResponder") continue;
44464
+ if (getImportedName(specifier) !== "PanResponder") continue;
42670
44465
  context.report({
42671
44466
  node: specifier,
42672
44467
  message: "PanResponder runs gesture handling on the JS thread, which stutters under load. Use react-native-gesture-handler (`Gesture.Pan()`) so gestures run on the native UI thread."
@@ -43457,7 +45252,7 @@ const rnNoSetNativeProps = defineRule({
43457
45252
  const callee = node.callee;
43458
45253
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43459
45254
  if (!isNodeOfType(callee, "MemberExpression")) return;
43460
- if (!isStaticMemberNamed(callee.object, "current")) return;
45255
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43461
45256
  context.report({
43462
45257
  node,
43463
45258
  message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
@@ -43596,7 +45391,7 @@ const rnPreferExpoImage = defineRule({
43596
45391
  for (const specifier of node.specifiers ?? []) {
43597
45392
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
43598
45393
  if (specifier.importKind === "type") continue;
43599
- const importedName = getImportedName$1(specifier);
45394
+ const importedName = getImportedName(specifier);
43600
45395
  if (importedName !== "Image" && importedName !== "ImageBackground") continue;
43601
45396
  flaggedImports.push({
43602
45397
  localName: specifier.local.name,
@@ -43658,7 +45453,7 @@ const rnPreferPressable = defineRule({
43658
45453
  for (const specifier of node.specifiers ?? []) {
43659
45454
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
43660
45455
  if (specifier.importKind === "type") continue;
43661
- const importedName = getImportedName$1(specifier);
45456
+ const importedName = getImportedName(specifier);
43662
45457
  if (!importedName || !TOUCHABLE_COMPONENTS.has(importedName)) continue;
43663
45458
  context.report({
43664
45459
  node: specifier,
@@ -43685,14 +45480,15 @@ const analyzeGestureChain = (expression) => {
43685
45480
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43686
45481
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43687
45482
  const methodName = callee.property.name;
43688
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
45483
+ const receiver = stripParenExpression(callee.object);
45484
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43689
45485
  factoryName: methodName,
43690
45486
  chainMethodNames,
43691
45487
  numberOfTapsArgument
43692
45488
  };
43693
45489
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43694
45490
  chainMethodNames.push(methodName);
43695
- cursor = callee.object;
45491
+ cursor = receiver;
43696
45492
  }
43697
45493
  return null;
43698
45494
  };
@@ -43759,7 +45555,7 @@ const rnPreferReanimated = defineRule({
43759
45555
  for (const specifier of node.specifiers ?? []) {
43760
45556
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
43761
45557
  if (specifier.importKind === "type") continue;
43762
- const importedName = getImportedName$1(specifier);
45558
+ const importedName = getImportedName(specifier);
43763
45559
  if (!importedName || !JS_THREAD_ANIMATION_IMPORTS.has(importedName)) continue;
43764
45560
  const suggestion = importedName === "LayoutAnimation" ? "Your users see stutter when LayoutAnimation runs on the JS thread." : "Your users see stutter when Animated from react-native runs on the JS thread.";
43765
45561
  context.report({
@@ -44217,9 +46013,9 @@ const roleHasRequiredAriaProps = defineRule({
44217
46013
  if (!HTML_TAGS.has(elementType)) return;
44218
46014
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44219
46015
  if (!roleAttribute) return;
44220
- const roleValue = getJsxPropStringValue(roleAttribute);
44221
- if (roleValue === null) return;
44222
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
46016
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
46017
+ if (roleCandidates === null) return;
46018
+ const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44223
46019
  for (const role of roles) {
44224
46020
  const required = ROLE_REQUIRED_PROPS.get(role);
44225
46021
  if (!required) continue;
@@ -47336,7 +49132,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47336
49132
  };
47337
49133
  //#endregion
47338
49134
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47339
- const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
49135
+ const buildMessageDefault = (roles, propName) => {
49136
+ return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
49137
+ };
47340
49138
  const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
47341
49139
  const roleSupportsAriaProps = defineRule({
47342
49140
  id: "role-supports-aria-props",
@@ -47356,6 +49154,8 @@ const roleSupportsAriaProps = defineRule({
47356
49154
  const propName = propRawName.toLowerCase();
47357
49155
  if (!propName.startsWith("aria-")) continue;
47358
49156
  if (!ARIA_PROPERTIES.has(propName)) continue;
49157
+ const attributeValue = attributeNode.value;
49158
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47359
49159
  (ariaAttributes ??= []).push({
47360
49160
  attribute,
47361
49161
  propName
@@ -47364,17 +49164,21 @@ const roleSupportsAriaProps = defineRule({
47364
49164
  if (!ariaAttributes) return;
47365
49165
  const elementType = getElementType(node, context.settings);
47366
49166
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47367
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47368
- if (!role) return;
47369
- if (!VALID_ARIA_ROLES.has(role)) return;
49167
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
49168
+ if (roleCandidates === null || roleCandidates.length === 0) return;
49169
+ const supportedSets = [];
49170
+ for (const role of roleCandidates) {
49171
+ if (!VALID_ARIA_ROLES.has(role)) return;
49172
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
49173
+ if (!supported) return;
49174
+ supportedSets.push(supported);
49175
+ }
47370
49176
  const isImplicit = !roleAttribute;
47371
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47372
- if (!supported) return;
47373
49177
  for (const { attribute, propName } of ariaAttributes) {
47374
- if (supported.has(propName)) continue;
49178
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47375
49179
  context.report({
47376
49180
  node: attribute,
47377
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
49181
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47378
49182
  });
47379
49183
  }
47380
49184
  } })
@@ -47721,21 +49525,6 @@ const isInsideLoop = (descendant, ancestor) => {
47721
49525
  }
47722
49526
  return false;
47723
49527
  };
47724
- const isUseEffectEventSymbol = (symbol) => {
47725
- const initializer = symbol.initializer;
47726
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47727
- return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47728
- };
47729
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47730
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47731
- return Boolean(symbol && symbol.kind !== "import");
47732
- };
47733
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47734
- const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47735
- const initializer = symbol.initializer;
47736
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47737
- return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47738
- };
47739
49528
  const findEnclosingComponentOrHookFunction = (node) => {
47740
49529
  let current = node.parent;
47741
49530
  while (current) {
@@ -47925,8 +49714,7 @@ const rulesOfHooks = defineRule({
47925
49714
  },
47926
49715
  Identifier(node) {
47927
49716
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47928
- if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47929
- if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
49717
+ if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
47930
49718
  if (!isSameComponentOrHookScope(symbol, node)) return;
47931
49719
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47932
49720
  context.report({
@@ -48074,7 +49862,8 @@ const serverAfterNonblocking = defineRule({
48074
49862
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48075
49863
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48076
49864
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48077
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
49865
+ const receiver = stripParenExpression(node.callee.object);
49866
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48078
49867
  if (!objectName) return;
48079
49868
  const methodName = node.callee.property.name;
48080
49869
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48750,7 +50539,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48750
50539
  };
48751
50540
  const ascendMemberChain = (referenceIdentifier) => {
48752
50541
  let chainTip = referenceIdentifier;
48753
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
50542
+ while (chainTip.parent) {
50543
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
50544
+ chainTip = chainTip.parent;
50545
+ continue;
50546
+ }
50547
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
50548
+ chainTip = chainTip.parent;
50549
+ continue;
50550
+ }
50551
+ break;
50552
+ }
48754
50553
  return chainTip;
48755
50554
  };
48756
50555
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48775,7 +50574,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48775
50574
  const callee = callExpression.callee;
48776
50575
  if (isNodeOfType(callee, "MemberExpression")) {
48777
50576
  const methodName = getMemberPropertyName$1(callee);
48778
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
50577
+ const calleeReceiver = stripParenExpression(callee.object);
50578
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48779
50579
  }
48780
50580
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48781
50581
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49505,7 +51305,7 @@ const walkServerFnChain = (outerNode) => {
49505
51305
  };
49506
51306
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49507
51307
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49508
- let currentNode = outerNode.callee.object;
51308
+ let currentNode = stripParenExpression(outerNode.callee.object);
49509
51309
  while (isNodeOfType(currentNode, "CallExpression")) {
49510
51310
  const calleeName = getCalleeName$2(currentNode);
49511
51311
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49516,7 +51316,7 @@ const walkServerFnChain = (outerNode) => {
49516
51316
  }
49517
51317
  }
49518
51318
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49519
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
51319
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49520
51320
  else break;
49521
51321
  }
49522
51322
  return result;
@@ -50169,7 +51969,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50169
51969
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50170
51970
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50171
51971
  if (methodName) methodNames.unshift(methodName);
50172
- currentNode = currentNode.callee.object;
51972
+ currentNode = stripParenExpression(currentNode.callee.object);
50173
51973
  }
50174
51974
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50175
51975
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -50389,7 +52189,7 @@ const useLazyMotion = defineRule({
50389
52189
  if (node.specifiers?.some((specifier) => {
50390
52190
  if (!isNodeOfType(specifier, "ImportSpecifier")) return false;
50391
52191
  if (specifier.importKind === "type") return false;
50392
- return getImportedName$1(specifier) === "motion";
52192
+ return getImportedName(specifier) === "motion";
50393
52193
  })) context.report({
50394
52194
  node,
50395
52195
  message: "Importing \"motion\" ships about 30 kb of extra code and slows page load. Use \"m\" with LazyMotion instead."
@@ -51416,6 +53216,18 @@ const reactDoctorRules = [
51416
53216
  requires: [...new Set(["react", ...displayName.requires ?? []])]
51417
53217
  }
51418
53218
  },
53219
+ {
53220
+ key: "react-doctor/effect-listener-cleanup-mismatch",
53221
+ id: "effect-listener-cleanup-mismatch",
53222
+ source: "react-doctor",
53223
+ originallyExternal: false,
53224
+ rule: {
53225
+ ...effectListenerCleanupMismatch,
53226
+ framework: "global",
53227
+ category: "Bugs",
53228
+ requires: [...new Set(["react", ...effectListenerCleanupMismatch.requires ?? []])]
53229
+ }
53230
+ },
51419
53231
  {
51420
53232
  key: "react-doctor/effect-needs-cleanup",
51421
53233
  id: "effect-needs-cleanup",
@@ -53251,6 +55063,18 @@ const reactDoctorRules = [
53251
55063
  category: "Bugs"
53252
55064
  }
53253
55065
  },
55066
+ {
55067
+ key: "react-doctor/no-locale-format-in-render",
55068
+ id: "no-locale-format-in-render",
55069
+ source: "react-doctor",
55070
+ originallyExternal: false,
55071
+ rule: {
55072
+ ...noLocaleFormatInRender,
55073
+ framework: "global",
55074
+ category: "Bugs",
55075
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
55076
+ }
55077
+ },
53254
55078
  {
53255
55079
  key: "react-doctor/no-long-transition-duration",
53256
55080
  id: "no-long-transition-duration",
@@ -53679,6 +55503,18 @@ const reactDoctorRules = [
53679
55503
  category: "Maintainability"
53680
55504
  }
53681
55505
  },
55506
+ {
55507
+ key: "react-doctor/no-stale-timer-ref",
55508
+ id: "no-stale-timer-ref",
55509
+ source: "react-doctor",
55510
+ originallyExternal: false,
55511
+ rule: {
55512
+ ...noStaleTimerRef,
55513
+ framework: "global",
55514
+ category: "Bugs",
55515
+ requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
55516
+ }
55517
+ },
53682
55518
  {
53683
55519
  key: "react-doctor/no-static-element-interactions",
53684
55520
  id: "no-static-element-interactions",
@@ -56127,6 +57963,7 @@ const CROSS_FILE_RULE_IDS = new Set([
56127
57963
  "nextjs-no-use-search-params-without-suspense",
56128
57964
  "no-dynamic-import-path",
56129
57965
  "no-full-lodash-import",
57966
+ "no-locale-format-in-render",
56130
57967
  "no-mutating-reducer-state",
56131
57968
  "prefer-dynamic-import",
56132
57969
  "rendering-hydration-mismatch-time",
@@ -56261,6 +58098,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
56261
58098
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
56262
58099
  ["no-dynamic-import-path", collectNearestManifestDependencies],
56263
58100
  ["no-full-lodash-import", collectNearestManifestDependencies],
58101
+ ["no-locale-format-in-render", collectNearestManifestDependencies],
56264
58102
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
56265
58103
  ["prefer-dynamic-import", collectNearestManifestDependencies],
56266
58104
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],