oxlint-plugin-react-doctor 0.7.2-dev.43d766f → 0.7.2-dev.6ae8e46

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 +46 -0
  2. package/dist/index.js +1504 -531
  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",
@@ -595,6 +601,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
595
601
  "parseInt",
596
602
  "parseFloat"
597
603
  ]);
604
+ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
605
+ "Date",
606
+ "Map",
607
+ "Set",
608
+ "WeakMap",
609
+ "WeakSet",
610
+ "WeakRef",
611
+ "RegExp",
612
+ "Error",
613
+ "URL",
614
+ "URLSearchParams",
615
+ "AbortController"
616
+ ]);
598
617
  const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
599
618
  "Boolean",
600
619
  "String",
@@ -674,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
674
693
  "unwatch",
675
694
  "unlisten",
676
695
  "unsub",
677
- "abort"
696
+ "abort",
697
+ "disconnect",
698
+ "unobserve",
699
+ "close"
678
700
  ]);
679
701
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
680
702
  "remove",
@@ -2235,6 +2257,39 @@ const anchorHasContent = defineRule({
2235
2257
  }
2236
2258
  });
2237
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
2238
2293
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2239
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).";
2240
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.";
@@ -2262,30 +2317,14 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2262
2317
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2263
2318
  const isInvalidHref = (value, validHrefs) => {
2264
2319
  if (validHrefs.has(value)) return false;
2265
- return value === "" || value === "#" || value === "javascript:void(0)";
2266
- };
2267
- const getStaticHrefValue = (value) => {
2268
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
2269
- if (isNodeOfType(value, "JSXExpressionContainer")) {
2270
- const expression = value.expression;
2271
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
2272
- if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
2273
- }
2274
- return null;
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2275
2322
  };
2276
- const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
2277
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
2323
+ const isNullishOrFragmentHref = (value) => {
2278
2324
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2279
2325
  const expression = value.expression;
2280
2326
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2281
- if (isNodeOfType(expression, "Literal")) {
2282
- if (expression.value === null) return true;
2283
- if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2284
- }
2285
- if (isNodeOfType(expression, "TemplateLiteral")) {
2286
- const staticValue = getStaticTemplateLiteralValue(expression);
2287
- return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2288
- }
2327
+ if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2289
2328
  }
2290
2329
  if (isNodeOfType(value, "JSXFragment")) return true;
2291
2330
  return false;
@@ -2318,9 +2357,10 @@ const anchorIsValid = defineRule({
2318
2357
  });
2319
2358
  return;
2320
2359
  }
2321
- 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)) {
2322
2362
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2323
- if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2363
+ if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2324
2364
  context.report({
2325
2365
  node: node.name,
2326
2366
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3376,6 +3416,25 @@ const ariaRole = defineRule({
3376
3416
  if (!roleAttribute) return;
3377
3417
  const elementType = getElementType(node, context.settings);
3378
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
+ };
3379
3438
  const value = roleAttribute.value;
3380
3439
  if (!value) {
3381
3440
  context.report({
@@ -3392,22 +3451,7 @@ const ariaRole = defineRule({
3392
3451
  });
3393
3452
  return;
3394
3453
  }
3395
- const stringValue = value.value;
3396
- if (stringValue.trim().length === 0) {
3397
- context.report({
3398
- node: roleAttribute,
3399
- message: buildBaseMessage("")
3400
- });
3401
- return;
3402
- }
3403
- const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
3404
- for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3405
- context.report({
3406
- node: roleAttribute,
3407
- message: buildBaseMessage(` \`${token}\` is not one.`)
3408
- });
3409
- return;
3410
- }
3454
+ reportFirstInvalidCandidate([value.value]);
3411
3455
  return;
3412
3456
  }
3413
3457
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3428,6 +3472,11 @@ const ariaRole = defineRule({
3428
3472
  });
3429
3473
  return;
3430
3474
  }
3475
+ const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3476
+ if (resolvedCandidates !== null) {
3477
+ reportFirstInvalidCandidate(resolvedCandidates);
3478
+ return;
3479
+ }
3431
3480
  return;
3432
3481
  }
3433
3482
  context.report({
@@ -5144,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
5144
5193
  const callee = node.callee;
5145
5194
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5146
5195
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5147
- if (!isWebStorageObject(callee.object)) return;
5196
+ if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5148
5197
  const keyArgument = node.arguments?.[0];
5149
5198
  if (!keyArgument) return;
5150
5199
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6084,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
6084
6133
  })
6085
6134
  });
6086
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
6087
6145
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6088
6146
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6089
6147
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6090
6148
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6091
6149
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6092
- const isJsonStringifyCall = (node) => {
6093
- if (!isNodeOfType(node, "CallExpression")) return false;
6094
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6095
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6096
- if (node.callee.object.name !== "JSON") return false;
6097
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6098
- return node.callee.property.name === "stringify";
6099
- };
6150
+ const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6100
6151
  const resolveStringKey = (keyArg, context) => {
6101
6152
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6102
6153
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6115,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
6115
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.",
6116
6167
  create: (context) => ({ CallExpression(node) {
6117
6168
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6118
- if (!isNodeOfType(node.callee.object, "Identifier")) return;
6119
- 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;
6120
6172
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6121
6173
  if (node.callee.property.name !== "setItem") return;
6122
6174
  const keyArg = node.arguments?.[0];
@@ -6129,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
6129
6181
  if (!isJsonStringifyCall(valueArg)) return;
6130
6182
  context.report({
6131
6183
  node: keyArg,
6132
- 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").`
6133
6185
  });
6134
6186
  } })
6135
6187
  });
@@ -7610,6 +7662,96 @@ const displayName = defineRule({
7610
7662
  }
7611
7663
  });
7612
7664
  //#endregion
7665
+ //#region src/plugin/utils/is-react-hook-name.ts
7666
+ const isReactHookName = (name) => {
7667
+ if (!name.startsWith("use")) return false;
7668
+ if (name.length === 3) return true;
7669
+ const fourthCharacter = name.charCodeAt(3);
7670
+ return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
7671
+ };
7672
+ //#endregion
7673
+ //#region src/plugin/utils/is-react-component-or-hook-name.ts
7674
+ const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
7675
+ //#endregion
7676
+ //#region src/plugin/utils/component-or-hook-display-name.ts
7677
+ const hocWrapperCalleeName = (callee) => {
7678
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
7679
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
7680
+ return null;
7681
+ };
7682
+ const displayNameFromFunctionBinding = (functionNode) => {
7683
+ let current = functionNode;
7684
+ for (;;) {
7685
+ const parent = current.parent;
7686
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
7687
+ const calleeName = hocWrapperCalleeName(parent.callee);
7688
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
7689
+ current = parent;
7690
+ continue;
7691
+ }
7692
+ }
7693
+ break;
7694
+ }
7695
+ const binding = current.parent;
7696
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
7697
+ return null;
7698
+ };
7699
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
7700
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
7701
+ return displayNameFromFunctionBinding(functionNode);
7702
+ };
7703
+ //#endregion
7704
+ //#region src/plugin/utils/find-enclosing-function.ts
7705
+ const findEnclosingFunction = (node) => {
7706
+ let cursor = node.parent;
7707
+ while (cursor) {
7708
+ if (isFunctionLike$1(cursor)) return cursor;
7709
+ cursor = cursor.parent ?? null;
7710
+ }
7711
+ return null;
7712
+ };
7713
+ //#endregion
7714
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
7715
+ const enclosingComponentOrHookName = (node) => {
7716
+ const functionNode = findEnclosingFunction(node);
7717
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
7718
+ };
7719
+ //#endregion
7720
+ //#region src/plugin/utils/is-result-discarded-call.ts
7721
+ const isResultDiscardedCall = (callExpression) => {
7722
+ let node = callExpression;
7723
+ let parent = node.parent;
7724
+ while (parent) {
7725
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
7726
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
7727
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7728
+ if (isNodeOfType(parent, "ChainExpression")) {
7729
+ node = parent;
7730
+ parent = node.parent;
7731
+ continue;
7732
+ }
7733
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
7734
+ node = parent;
7735
+ parent = node.parent;
7736
+ continue;
7737
+ }
7738
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
7739
+ node = parent;
7740
+ parent = node.parent;
7741
+ continue;
7742
+ }
7743
+ if (isNodeOfType(parent, "SequenceExpression")) {
7744
+ const expressions = parent.expressions ?? [];
7745
+ if (expressions[expressions.length - 1] !== node) return true;
7746
+ node = parent;
7747
+ parent = node.parent;
7748
+ continue;
7749
+ }
7750
+ return false;
7751
+ }
7752
+ return false;
7753
+ };
7754
+ //#endregion
7613
7755
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7614
7756
  const walkInsideStatementBlocks = (node, visitor) => {
7615
7757
  if (!node || typeof node !== "object") return;
@@ -7761,6 +7903,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7761
7903
  };
7762
7904
  //#endregion
7763
7905
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
7906
+ const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
7907
+ const RESOURCE_NOUN_BY_KIND = {
7908
+ subscribe: "subscription",
7909
+ timer: "timer",
7910
+ socket: "connection"
7911
+ };
7912
+ const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
7913
+ const isSubscribeOrObserveCall = (node) => {
7914
+ if (isSubscribeLikeCallExpression(node)) return true;
7915
+ return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
7916
+ };
7764
7917
  const findSubscribeLikeUsages = (callback) => {
7765
7918
  const usages = [];
7766
7919
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7772,6 +7925,14 @@ const findSubscribeLikeUsages = (callback) => {
7772
7925
  }
7773
7926
  walkAst(callback, (child) => {
7774
7927
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
7928
+ if (isSocketConstruction(child)) {
7929
+ usages.push({
7930
+ kind: "socket",
7931
+ node: child,
7932
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
7933
+ });
7934
+ return;
7935
+ }
7775
7936
  if (!isNodeOfType(child, "CallExpression")) return;
7776
7937
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7777
7938
  usages.push({
@@ -7781,7 +7942,7 @@ const findSubscribeLikeUsages = (callback) => {
7781
7942
  });
7782
7943
  return;
7783
7944
  }
7784
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7945
+ 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({
7785
7946
  kind: "subscribe",
7786
7947
  node: child,
7787
7948
  resourceName: child.callee.property.name
@@ -7797,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
7797
7958
  };
7798
7959
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7799
7960
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
7961
+ walkAst(effectCallback.body, (child) => {
7962
+ if (!isSubscribeOrObserveCall(child)) return;
7963
+ if (!isNodeOfType(child, "CallExpression")) return;
7964
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
7965
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
7966
+ bindings.subscriptionNames.add(child.callee.object.name);
7967
+ });
7800
7968
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7801
7969
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7802
7970
  for (const declarator of child.declarations ?? []) {
@@ -7804,7 +7972,12 @@ const collectCleanupBindings = (effectCallback) => {
7804
7972
  const bindingName = declarator.id.name;
7805
7973
  bindings.effectScopeVariableNames.add(bindingName);
7806
7974
  const init = declarator.init;
7807
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
7975
+ if (!init) continue;
7976
+ if (isSocketConstruction(init)) {
7977
+ bindings.subscriptionNames.add(bindingName);
7978
+ continue;
7979
+ }
7980
+ if (!isNodeOfType(init, "CallExpression")) continue;
7808
7981
  if (isSubscribeLikeCallExpression(init)) {
7809
7982
  bindings.subscriptionNames.add(bindingName);
7810
7983
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7834,6 +8007,22 @@ const getRangeStart = (node) => {
7834
8007
  const rangeStart = node.range?.[0];
7835
8008
  return typeof rangeStart === "number" ? rangeStart : null;
7836
8009
  };
8010
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8011
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8012
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8013
+ const releaseStarts = [];
8014
+ walkInsideStatementBlocks(callback.body, (child) => {
8015
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8016
+ const releaseStart = getRangeStart(child);
8017
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8018
+ });
8019
+ if (releaseStarts.length === 0) return usages;
8020
+ return usages.filter((usage) => {
8021
+ const usageStart = getRangeStart(usage.node);
8022
+ if (usageStart === null) return true;
8023
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8024
+ });
8025
+ };
7837
8026
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7838
8027
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
7839
8028
  const returnStart = getRangeStart(returnStatement);
@@ -7856,26 +8045,177 @@ const effectHasCleanupReturn = (callback, usages) => {
7856
8045
  });
7857
8046
  return didFindCleanupReturn;
7858
8047
  };
8048
+ const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8049
+ const isSelfReleasingListenerOptionProperty = (property) => {
8050
+ if (!isNodeOfType(property, "Property")) return false;
8051
+ const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
8052
+ if (keyName === "signal") return true;
8053
+ if (keyName !== "once") return false;
8054
+ return isNodeOfType(property.value, "Literal") && property.value.value === true;
8055
+ };
8056
+ const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
8057
+ const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8058
+ ["addEventListener", new Set(["removeEventListener", "abort"])],
8059
+ ["addListener", new Set([
8060
+ "removeListener",
8061
+ "off",
8062
+ "abort"
8063
+ ])],
8064
+ ["on", new Set([
8065
+ "off",
8066
+ "removeListener",
8067
+ "on"
8068
+ ])],
8069
+ ["subscribe", new Set(["unsubscribe", "unsub"])],
8070
+ ["sub", new Set(["unsub", "unsubscribe"])],
8071
+ ["watch", new Set(["unwatch", "close"])],
8072
+ ["listen", new Set(["unlisten", "close"])],
8073
+ [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8074
+ ]);
8075
+ const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8076
+ "cleanup",
8077
+ "dispose",
8078
+ "destroy",
8079
+ "teardown"
8080
+ ]);
8081
+ const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8082
+ const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8083
+ const getReleaseVerbName = (node) => {
8084
+ if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8085
+ const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8086
+ if (!isNodeOfType(callNode, "CallExpression")) return null;
8087
+ const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8088
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
8089
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8090
+ return null;
8091
+ };
8092
+ const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8093
+ const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8094
+ let didFindPairedRelease = false;
8095
+ walkAst(body, (child) => {
8096
+ if (didFindPairedRelease) return false;
8097
+ if (child !== body && isFunctionLike$1(child)) return false;
8098
+ const releaseVerbName = getReleaseVerbName(child);
8099
+ if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8100
+ didFindPairedRelease = true;
8101
+ return false;
8102
+ }
8103
+ });
8104
+ return didFindPairedRelease;
8105
+ };
8106
+ const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8107
+ const collectFileReleaseVerbNames = (anyNode) => {
8108
+ let programNode = anyNode;
8109
+ while (programNode.parent) programNode = programNode.parent;
8110
+ const cached = fileReleaseVerbNamesCache.get(programNode);
8111
+ if (cached) return cached;
8112
+ const releaseVerbNames = /* @__PURE__ */ new Set();
8113
+ walkAst(programNode, (child) => {
8114
+ const releaseVerbName = getReleaseVerbName(child);
8115
+ if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8116
+ });
8117
+ fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8118
+ return releaseVerbNames;
8119
+ };
8120
+ const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8121
+ const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8122
+ const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8123
+ if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8124
+ for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8125
+ return false;
8126
+ };
8127
+ const findRetainedFunctionLeak = (retainedFunction) => {
8128
+ if (!isFunctionLike$1(retainedFunction)) return null;
8129
+ const body = retainedFunction.body;
8130
+ if (!body) return null;
8131
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8132
+ let leak = null;
8133
+ walkAst(body, (child) => {
8134
+ if (leak !== null) return false;
8135
+ if (isFunctionLike$1(child)) return false;
8136
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8137
+ if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8138
+ leak = {
8139
+ kind: "socket",
8140
+ node: child,
8141
+ resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8142
+ };
8143
+ return false;
8144
+ }
8145
+ if (!isNodeOfType(child, "CallExpression")) return;
8146
+ if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8147
+ leak = {
8148
+ kind: "timer",
8149
+ node: child,
8150
+ resourceName: "setInterval"
8151
+ };
8152
+ return false;
8153
+ }
8154
+ if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8155
+ const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8156
+ if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8157
+ kind: "subscribe",
8158
+ node: child,
8159
+ resourceName: registrationVerbName
8160
+ };
8161
+ return false;
8162
+ }
8163
+ });
8164
+ return leak;
8165
+ };
8166
+ const isRetainedComponentScopeFunction = (functionNode) => {
8167
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8168
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8169
+ if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8170
+ return enclosingComponentOrHookName(functionNode) !== null;
8171
+ };
7859
8172
  const effectNeedsCleanup = defineRule({
7860
8173
  id: "effect-needs-cleanup",
7861
8174
  title: "Effect subscription or timer never cleaned up",
7862
8175
  severity: "error",
7863
8176
  tags: ["test-noise"],
7864
- 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.",
7865
- create: (context) => ({ CallExpression(node) {
7866
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7867
- const callback = getEffectCallback(node);
7868
- if (!callback) return;
7869
- const usages = findSubscribeLikeUsages(callback);
7870
- if (usages.length === 0) return;
7871
- if (effectHasCleanupReturn(callback, usages)) return;
7872
- const firstUsage = usages[0];
7873
- const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7874
- context.report({
7875
- node,
7876
- message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7877
- });
7878
- } })
8177
+ 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.",
8178
+ create: (context) => {
8179
+ const reportRetainedLeak = (retainedFunction) => {
8180
+ const leak = findRetainedFunctionLeak(retainedFunction);
8181
+ if (!leak) return;
8182
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8183
+ context.report({
8184
+ node: leak.node,
8185
+ 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.`
8186
+ });
8187
+ };
8188
+ return {
8189
+ CallExpression(node) {
8190
+ if (isHookCall$2(node, "useCallback")) {
8191
+ const retainedCallback = getEffectCallback(node);
8192
+ if (retainedCallback) reportRetainedLeak(retainedCallback);
8193
+ return;
8194
+ }
8195
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8196
+ const callback = getEffectCallback(node);
8197
+ if (!callback) return;
8198
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8199
+ if (usages.length === 0) return;
8200
+ if (effectHasCleanupReturn(callback, usages)) return;
8201
+ const firstUsage = usages[0];
8202
+ const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8203
+ context.report({
8204
+ node,
8205
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8206
+ });
8207
+ },
8208
+ FunctionDeclaration(node) {
8209
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8210
+ },
8211
+ ArrowFunctionExpression(node) {
8212
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8213
+ },
8214
+ FunctionExpression(node) {
8215
+ if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8216
+ }
8217
+ };
8218
+ }
7879
8219
  });
7880
8220
  //#endregion
7881
8221
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8434,17 +8774,6 @@ const closureCaptures = (functionNode, scopes) => {
8434
8774
  return computedCaptures;
8435
8775
  };
8436
8776
  //#endregion
8437
- //#region src/plugin/utils/is-react-hook-name.ts
8438
- const isReactHookName = (name) => {
8439
- if (!name.startsWith("use")) return false;
8440
- if (name.length === 3) return true;
8441
- const fourthCharacter = name.charCodeAt(3);
8442
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8443
- };
8444
- //#endregion
8445
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
8446
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8447
- //#endregion
8448
8777
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8449
8778
  const reactHocCalleeName = (callee) => {
8450
8779
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -8651,6 +8980,24 @@ const isAstDescendant = (inner, outer) => {
8651
8980
  return false;
8652
8981
  };
8653
8982
  //#endregion
8983
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
8984
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
8985
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
8986
+ if (importSource === null) return false;
8987
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
8988
+ };
8989
+ //#endregion
8990
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
8991
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
8992
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
8993
+ return Boolean(symbol && symbol.kind !== "import");
8994
+ };
8995
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
8996
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
8997
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
8998
+ return false;
8999
+ };
9000
+ //#endregion
8654
9001
  //#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
8655
9002
  /**
8656
9003
  * Symbol-stability helpers consumed by the `exhaustive-deps` rule.
@@ -8715,10 +9062,11 @@ const symbolHasStableHookOrigin = (symbol) => {
8715
9062
  }
8716
9063
  return false;
8717
9064
  };
8718
- const symbolHasUseEffectEventOrigin = (symbol) => {
9065
+ const symbolHasUseEffectEventOrigin = (symbol, scopes) => {
8719
9066
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
8720
9067
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
8721
- return getHookName(initializer.callee) === "useEffectEvent";
9068
+ if (getHookName(initializer.callee) !== "useEffectEvent") return false;
9069
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
8722
9070
  };
8723
9071
  const getFunctionValueNode = (symbol) => {
8724
9072
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
@@ -9455,7 +9803,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9455
9803
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9456
9804
  if (isNodeOfType(stripped, "Identifier")) {
9457
9805
  const depSymbol = context.scopes.symbolFor(stripped);
9458
- if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
9806
+ if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol, context.scopes)) {
9459
9807
  context.report({
9460
9808
  node: elementNode,
9461
9809
  message: buildEffectEventDepMessage()
@@ -9616,7 +9964,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9616
9964
  });
9617
9965
  //#endregion
9618
9966
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9619
- const EMPTY_VISITORS$5 = {};
9967
+ const EMPTY_VISITORS$6 = {};
9620
9968
  const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
9621
9969
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9622
9970
  const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
@@ -9628,7 +9976,7 @@ const expoNoNonInlinedEnv = defineRule({
9628
9976
  recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
9629
9977
  create: (context) => {
9630
9978
  const filename = normalizeFilename(context.filename ?? "");
9631
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9979
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9632
9980
  return {
9633
9981
  MemberExpression(node) {
9634
9982
  if (!node.computed) return;
@@ -9881,7 +10229,10 @@ const PRAGMA = "React";
9881
10229
  const isReactFunctionCall = (node, expectedCall) => {
9882
10230
  if (!isNodeOfType(node, "CallExpression")) return false;
9883
10231
  if (getCalleeName$2(node) !== expectedCall) return false;
9884
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10232
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10233
+ const receiver = stripParenExpression(node.callee.object);
10234
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10235
+ }
9885
10236
  return true;
9886
10237
  };
9887
10238
  //#endregion
@@ -11046,8 +11397,8 @@ const interactiveSupportsFocus = defineRule({
11046
11397
  if (node.attributes.length === 0) return;
11047
11398
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11048
11399
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11049
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
- if (!role) return;
11400
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11401
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11051
11402
  let hasInteractiveHandler = false;
11052
11403
  for (const attribute of node.attributes) {
11053
11404
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11061,11 +11412,16 @@ const interactiveSupportsFocus = defineRule({
11061
11412
  const elementType = getElementType(node, context.settings);
11062
11413
  if (!HTML_TAGS.has(elementType)) return;
11063
11414
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11064
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11065
- if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11066
11415
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11067
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11068
- const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
11416
+ const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11417
+ for (const role of roleCandidates) {
11418
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11419
+ if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11420
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11421
+ }
11422
+ const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11423
+ const roleDisplay = roleCandidates.join("' / '");
11424
+ const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11069
11425
  context.report({
11070
11426
  node,
11071
11427
  message
@@ -11254,16 +11610,6 @@ const collectHandlerReferencedNames = (root) => {
11254
11610
  return names;
11255
11611
  };
11256
11612
  //#endregion
11257
- //#region src/plugin/utils/find-enclosing-function.ts
11258
- const findEnclosingFunction = (node) => {
11259
- let cursor = node.parent;
11260
- while (cursor) {
11261
- if (isFunctionLike$1(cursor)) return cursor;
11262
- cursor = cursor.parent ?? null;
11263
- }
11264
- return null;
11265
- };
11266
- //#endregion
11267
11613
  //#region src/plugin/utils/get-function-binding-name.ts
11268
11614
  const getFunctionBindingIdentifier = (functionNode) => {
11269
11615
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -11963,14 +12309,15 @@ const jsCacheStorage = defineRule({
11963
12309
  "ArrowFunctionExpression:exit": exitFunctionScope,
11964
12310
  CallExpression(node) {
11965
12311
  if (!isMemberProperty(node.callee, "getItem")) return;
11966
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12312
+ const receiver = stripParenExpression(node.callee.object);
12313
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
11967
12314
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
11968
12315
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
11969
12316
  const storageKey = String(node.arguments[0].value);
11970
12317
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
11971
12318
  storageReadCounts.set(storageKey, readCount);
11972
12319
  if (readCount === 2) {
11973
- const storageName = node.callee.object.name;
12320
+ const storageName = receiver.name;
11974
12321
  context.report({
11975
12322
  node,
11976
12323
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -11984,13 +12331,16 @@ const jsCacheStorage = defineRule({
11984
12331
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
11985
12332
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
11986
12333
  const callee = callExpression.callee;
11987
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
11988
- if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
11989
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
11990
- const receiver = callee.object;
11991
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
11992
- return true;
12334
+ if (isNodeOfType(callee, "MemberExpression")) {
12335
+ const receiver = stripParenExpression(callee.object);
12336
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12337
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12338
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12339
+ return true;
12340
+ }
12341
+ return false;
11993
12342
  }
12343
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
11994
12344
  return false;
11995
12345
  };
11996
12346
  const isChainPassThroughCall = (callExpression) => {
@@ -12002,10 +12352,7 @@ const isChainPassThroughCall = (callExpression) => {
12002
12352
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12003
12353
  let cursor = receiverNode;
12004
12354
  while (cursor) {
12005
- if (isNodeOfType(cursor, "ChainExpression")) {
12006
- cursor = cursor.expression;
12007
- continue;
12008
- }
12355
+ cursor = stripParenExpression(cursor);
12009
12356
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12010
12357
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12011
12358
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12081,10 +12428,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12081
12428
  let hops = 0;
12082
12429
  while (cursor && hops < 12) {
12083
12430
  hops += 1;
12084
- if (isNodeOfType(cursor, "ChainExpression")) {
12085
- cursor = cursor.expression;
12086
- continue;
12087
- }
12431
+ cursor = stripParenExpression(cursor);
12088
12432
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12089
12433
  const callee = cursor.callee;
12090
12434
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12108,10 +12452,7 @@ const isSmallLiteralArray = (node) => {
12108
12452
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12109
12453
  let cursor = receiverNode;
12110
12454
  while (cursor) {
12111
- if (isNodeOfType(cursor, "ChainExpression")) {
12112
- cursor = cursor.expression;
12113
- continue;
12114
- }
12455
+ cursor = stripParenExpression(cursor);
12115
12456
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12116
12457
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12117
12458
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12176,7 +12517,7 @@ const jsCombineIterations = defineRule({
12176
12517
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12177
12518
  const outerMethod = node.callee.property.name;
12178
12519
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12179
- const innerCall = node.callee.object;
12520
+ const innerCall = stripParenExpression(node.callee.object);
12180
12521
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12181
12522
  const innerMethod = innerCall.callee.property.name;
12182
12523
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12249,10 +12590,10 @@ const jsFlatmapFilter = defineRule({
12249
12590
  if (!filterArgument) return;
12250
12591
  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;
12251
12592
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12252
- const innerCall = node.callee.object;
12593
+ const innerCall = stripParenExpression(node.callee.object);
12253
12594
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12254
12595
  if (innerCall.callee.property.name !== "map") return;
12255
- const receiver = innerCall.callee.object;
12596
+ const receiver = stripParenExpression(innerCall.callee.object);
12256
12597
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12257
12598
  const elements = receiver.elements ?? [];
12258
12599
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13512,7 +13853,7 @@ const jsTosortedImmutable = defineRule({
13512
13853
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13513
13854
  create: (context) => ({ CallExpression(node) {
13514
13855
  if (!isMemberProperty(node.callee, "sort")) return;
13515
- const receiver = node.callee.object;
13856
+ const receiver = stripParenExpression(node.callee.object);
13516
13857
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13517
13858
  const spreadArgument = receiver.elements[0].argument;
13518
13859
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18549,7 +18890,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18549
18890
  };
18550
18891
  const describeClientSideNavigation = (node) => {
18551
18892
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18552
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18893
+ const receiver = stripParenExpression(node.callee.object);
18894
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18553
18895
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18554
18896
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18555
18897
  }
@@ -19169,7 +19511,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19169
19511
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19170
19512
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19171
19513
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19172
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19514
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19173
19515
  return node.callee.property.name;
19174
19516
  };
19175
19517
  const isMutatingFetchCall = (node) => {
@@ -19183,7 +19525,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19183
19525
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19184
19526
  const { property, object } = node.callee;
19185
19527
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19186
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19528
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19187
19529
  return true;
19188
19530
  };
19189
19531
  const getDbCallDescription = (node) => {
@@ -19191,7 +19533,8 @@ const getDbCallDescription = (node) => {
19191
19533
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19192
19534
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19193
19535
  const methodName = node.callee.property.name;
19194
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19536
+ const receiver = stripParenExpression(node.callee.object);
19537
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19195
19538
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19196
19539
  };
19197
19540
  const findSideEffect = (node, options = {}) => {
@@ -20591,13 +20934,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20591
20934
  if (isNodeOfType(current, "CallExpression")) {
20592
20935
  let node = ref.identifier;
20593
20936
  let parent = node.parent;
20594
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20937
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20595
20938
  node = parent;
20596
20939
  parent = node.parent;
20597
20940
  }
20598
20941
  if (current.callee === node) return current;
20599
20942
  }
20600
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20943
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20601
20944
  return null;
20602
20945
  };
20603
20946
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20732,7 +21075,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20732
21075
  const importDeclaration = declarationNode.parent;
20733
21076
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20734
21077
  }));
20735
- const isHookCallee = (analysis, node, hookName) => {
21078
+ const isHookCallee$1 = (analysis, node, hookName) => {
20736
21079
  if (!node) return false;
20737
21080
  if (isNodeOfType(node, "Identifier")) {
20738
21081
  if (node.name === hookName) return true;
@@ -20741,15 +21084,19 @@ const isHookCallee = (analysis, node, hookName) => {
20741
21084
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20742
21085
  return false;
20743
21086
  }
20744
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21087
+ if (isNodeOfType(node, "MemberExpression")) {
21088
+ const receiver = stripParenExpression(node.object);
21089
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21090
+ }
20745
21091
  return false;
20746
21092
  };
20747
21093
  const isUseEffect = (node) => {
20748
21094
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20749
21095
  const callee = node.callee;
20750
21096
  if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
20751
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
20752
- return false;
21097
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21098
+ const receiver = stripParenExpression(callee.object);
21099
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20753
21100
  };
20754
21101
  const getEffectFn = (analysis, node) => {
20755
21102
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20777,7 +21124,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20777
21124
  const node = def.node;
20778
21125
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20779
21126
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20780
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21127
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20781
21128
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20782
21129
  const elements = node.id.elements ?? [];
20783
21130
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20788,7 +21135,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20788
21135
  const node = def.node;
20789
21136
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20790
21137
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20791
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21138
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20792
21139
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20793
21140
  const elements = node.id.elements ?? [];
20794
21141
  if (elements.length !== 2) return false;
@@ -20835,7 +21182,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20835
21182
  const node = def.node;
20836
21183
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20837
21184
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20838
- return isHookCallee(analysis, node.init.callee, "useRef");
21185
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20839
21186
  }));
20840
21187
  const isRefCurrent = (ref) => {
20841
21188
  const parent = ref.identifier.parent;
@@ -20848,11 +21195,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20848
21195
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20849
21196
  const isPropCallbackInvocationRef = (analysis, ref) => {
20850
21197
  if (!isPropAlias(analysis, ref)) return false;
20851
- const identifier = ref.identifier;
20852
- const parent = identifier.parent;
21198
+ let effectiveNode = ref.identifier;
21199
+ let parent = effectiveNode.parent;
21200
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21201
+ effectiveNode = parent;
21202
+ parent = effectiveNode.parent;
21203
+ }
20853
21204
  if (!parent) return false;
20854
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20855
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21205
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21206
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20856
21207
  const memberParent = parent.parent;
20857
21208
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20858
21209
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20863,7 +21214,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20863
21214
  };
20864
21215
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20865
21216
  const getUseStateDecl = (analysis, ref) => {
20866
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21217
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20867
21218
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20868
21219
  return node ?? null;
20869
21220
  };
@@ -21068,7 +21419,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21068
21419
  const noAdjustStateOnPropChange = defineRule({
21069
21420
  id: "no-adjust-state-on-prop-change",
21070
21421
  title: "State synced to a prop inside an effect",
21071
- severity: "error",
21422
+ severity: "warn",
21072
21423
  tags: ["test-noise"],
21073
21424
  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",
21074
21425
  create: (context) => ({ CallExpression(node) {
@@ -21109,7 +21460,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21109
21460
  "summary",
21110
21461
  "textarea"
21111
21462
  ]);
21463
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21464
+ const value = attribute.value;
21465
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21466
+ const expression = value.expression;
21467
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21468
+ };
21469
+ const DISABLEABLE_TAGS = new Set([
21470
+ "button",
21471
+ "input",
21472
+ "select",
21473
+ "textarea"
21474
+ ]);
21112
21475
  const isNativelyFocusable = (tagName, openingElement) => {
21476
+ if (DISABLEABLE_TAGS.has(tagName)) {
21477
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21478
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21479
+ }
21113
21480
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21114
21481
  switch (tagName) {
21115
21482
  case "input": {
@@ -21123,7 +21490,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21123
21490
  case "a":
21124
21491
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21125
21492
  case "audio":
21126
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21493
+ case "video": {
21494
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21495
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21496
+ }
21127
21497
  default: return false;
21128
21498
  }
21129
21499
  };
@@ -22087,7 +22457,8 @@ const SECOND_INDEX_METHODS = new Set([
22087
22457
  "some"
22088
22458
  ]);
22089
22459
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22090
- const isPositionallyStableIterationReceiver = (receiver) => {
22460
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22461
+ const receiver = stripParenExpression(receiverNode);
22091
22462
  if (isAllLiteralArrayExpression(receiver)) return true;
22092
22463
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22093
22464
  const only = receiver.elements[0];
@@ -22098,17 +22469,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22098
22469
  }
22099
22470
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22100
22471
  const callee = receiver.callee;
22101
- 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;
22472
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22102
22473
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22103
22474
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22104
22475
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22105
22476
  return false;
22106
22477
  };
22107
- const isArrayFromMapperCallback = (parentCall, callback) => {
22108
- if (parentCall.arguments[1] !== callback) return false;
22109
- const callee = parentCall.callee;
22110
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
22111
- };
22478
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22112
22479
  const isArrayFromSourcePositionallyStable = (source) => {
22113
22480
  if (isNodeOfType(source, "ObjectExpression")) {
22114
22481
  for (const property of source.properties ?? []) {
@@ -22167,18 +22534,12 @@ const expressionUsesIndex = (expression, paramName) => {
22167
22534
  return false;
22168
22535
  }
22169
22536
  if (isNodeOfType(expression, "CallExpression")) {
22170
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
22537
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22171
22538
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22172
22539
  }
22173
22540
  return false;
22174
22541
  };
22175
- const isReactCloneElement = (callExpression) => {
22176
- const callee = callExpression.callee;
22177
- if (!isNodeOfType(callee, "MemberExpression")) return false;
22178
- if (!isNodeOfType(callee.property, "Identifier")) return false;
22179
- if (callee.property.name !== "cloneElement") return false;
22180
- return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
22181
- };
22542
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22182
22543
  const noArrayIndexKey = defineRule({
22183
22544
  id: "no-array-index-key",
22184
22545
  title: "Array index used as a key",
@@ -22477,6 +22838,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22477
22838
  const section = manifest[sectionName];
22478
22839
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22479
22840
  });
22841
+ const declaresDependency = (manifest, dependencyName) => {
22842
+ for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22843
+ return false;
22844
+ };
22480
22845
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22481
22846
  const classifyPackagePlatform = (filename) => {
22482
22847
  const manifest = readNearestPackageManifest(filename);
@@ -22493,9 +22858,7 @@ const classifyPackagePlatform = (filename) => {
22493
22858
  return result;
22494
22859
  };
22495
22860
  //#endregion
22496
- //#region src/plugin/utils/is-react-native-file.ts
22497
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22498
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22861
+ //#region src/plugin/utils/is-package-nested-below-project-root.ts
22499
22862
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22500
22863
  const resolveRealDirectory = (directory) => {
22501
22864
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22516,6 +22879,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22516
22879
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22517
22880
  return realPackageDirectory.startsWith(rootPrefix);
22518
22881
  };
22882
+ //#endregion
22883
+ //#region src/plugin/utils/is-react-native-file.ts
22884
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22885
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22519
22886
  const classifyReactNativeFileTarget = (context) => {
22520
22887
  const rawFilename = context.filename;
22521
22888
  if (!rawFilename) return "unknown";
@@ -22896,7 +23263,7 @@ const isAsyncFunctionLike = (node) => {
22896
23263
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22897
23264
  return false;
22898
23265
  };
22899
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23266
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22900
23267
  "forEach",
22901
23268
  "map",
22902
23269
  "filter",
@@ -22917,31 +23284,51 @@ const runsOnEffectDispatch = (functionNode) => {
22917
23284
  if (parent.callee === functionNode) return true;
22918
23285
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22919
23286
  const callee = parent.callee;
22920
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23287
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22921
23288
  };
22922
23289
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
22923
- const isGuardWithTerminatingBranch = (statement) => {
22924
- if (!isNodeOfType(statement, "IfStatement")) return null;
22925
- if (statement.alternate) return null;
22926
- const consequent = statement.consequent;
22927
- if (isTerminatingStatement(consequent)) return consequent;
22928
- if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
22929
- return null;
22930
- };
22931
- const countStatementSequenceSetStateCalls = (statements, context) => {
23290
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23291
+ const analyzeStatementSequence = (statements, context) => {
22932
23292
  let fallThroughCount = 0;
22933
- let maxTerminatingPathCount = 0;
23293
+ let maxTerminatedCount = 0;
22934
23294
  for (const statement of statements) {
22935
- if (isFunctionLike$1(statement)) continue;
22936
- const guardBranch = isGuardWithTerminatingBranch(statement);
22937
- if (guardBranch) {
22938
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23295
+ if (isNodeOfType(statement, "IfStatement")) {
23296
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23297
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23298
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23299
+ fallThroughCount: 0,
23300
+ maxTerminatedCount: 0,
23301
+ doAllPathsTerminate: false
23302
+ };
23303
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23304
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23305
+ fallThroughCount: 0,
23306
+ maxTerminatedCount,
23307
+ doAllPathsTerminate: true
23308
+ };
23309
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23310
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22939
23311
  continue;
22940
23312
  }
22941
- if (isTerminatingStatement(statement)) break;
23313
+ if (isTerminatingStatement(statement)) {
23314
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23315
+ return {
23316
+ fallThroughCount: 0,
23317
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23318
+ doAllPathsTerminate: true
23319
+ };
23320
+ }
22942
23321
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22943
23322
  }
22944
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23323
+ return {
23324
+ fallThroughCount,
23325
+ maxTerminatedCount,
23326
+ doAllPathsTerminate: false
23327
+ };
23328
+ };
23329
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23330
+ const summary = analyzeStatementSequence(statements, context);
23331
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22945
23332
  };
22946
23333
  const collectLocalHelperFunctions = (root) => {
22947
23334
  const helpersByName = /* @__PURE__ */ new Map();
@@ -22968,14 +23355,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
22968
23355
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
22969
23356
  return parent.parent === effectCallback.body;
22970
23357
  };
23358
+ const countFunctionBodySetStateCalls = (functionNode, context) => {
23359
+ if (isAsyncFunctionLike(functionNode)) return 0;
23360
+ const body = functionNode.body;
23361
+ if (!body) return 0;
23362
+ return countMaxPathSetStateCalls(body, context);
23363
+ };
22971
23364
  const countMaxPathSetStateCalls = (node, context) => {
22972
23365
  if (!node || typeof node !== "object") return 0;
22973
- if (isAsyncFunctionLike(node)) return 0;
23366
+ if (isFunctionLike$1(node)) return 0;
22974
23367
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
22975
23368
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
22976
23369
  const consequent = node.consequent;
22977
23370
  const alternate = node.alternate;
22978
- return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
23371
+ const testCount = countMaxPathSetStateCalls(node.test, context);
23372
+ const thenCount = countMaxPathSetStateCalls(consequent, context);
23373
+ const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
23374
+ return testCount + Math.max(thenCount, elseCount);
22979
23375
  }
22980
23376
  if (isNodeOfType(node, "SwitchStatement")) {
22981
23377
  let maxRunSetters = 0;
@@ -23005,28 +23401,31 @@ const countMaxPathSetStateCalls = (node, context) => {
23005
23401
  }
23006
23402
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23007
23403
  let nestedSettersInArgs = 0;
23008
- for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23404
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23009
23405
  return 1 + nestedSettersInArgs;
23010
23406
  }
23011
23407
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23012
23408
  const helperFunction = context.helpersByName.get(node.callee.name);
23013
23409
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23014
23410
  context.activeHelpers.add(helperFunction);
23015
- let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23411
+ let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23016
23412
  context.activeHelpers.delete(helperFunction);
23017
23413
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23018
23414
  return helperCount;
23019
23415
  }
23020
23416
  }
23021
- const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23417
+ const countChild = (child) => {
23418
+ if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23419
+ return countMaxPathSetStateCalls(child, context);
23420
+ };
23022
23421
  let total = 0;
23023
23422
  const nodeRecord = node;
23024
23423
  for (const key of Object.keys(nodeRecord)) {
23025
23424
  if (key === "parent") continue;
23026
23425
  const child = nodeRecord[key];
23027
23426
  if (Array.isArray(child)) {
23028
- for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
23029
- } else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
23427
+ for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
23428
+ } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
23030
23429
  }
23031
23430
  return total;
23032
23431
  };
@@ -23060,7 +23459,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23060
23459
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23061
23460
  const firstStatement = (body.body ?? [])[0];
23062
23461
  if (!firstStatement) return false;
23063
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23462
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23463
+ const consequent = firstStatement.consequent;
23464
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23064
23465
  return mentionsDevEnvFlag(firstStatement.test);
23065
23466
  };
23066
23467
  const noCascadingSetState = defineRule({
@@ -23075,7 +23476,7 @@ const noCascadingSetState = defineRule({
23075
23476
  const callback = getEffectCallback(node);
23076
23477
  if (!callback) return;
23077
23478
  if (isDevOnlyGuardedEffect(callback)) return;
23078
- const setStateCallCount = countMaxPathSetStateCalls(callback, {
23479
+ const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23079
23480
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23080
23481
  activeHelpers: /* @__PURE__ */ new Set(),
23081
23482
  effectCallback: callback
@@ -23419,40 +23820,6 @@ const noCloneElement = defineRule({
23419
23820
  } })
23420
23821
  });
23421
23822
  //#endregion
23422
- //#region src/plugin/utils/component-or-hook-display-name.ts
23423
- const hocWrapperCalleeName = (callee) => {
23424
- if (isNodeOfType(callee, "Identifier")) return callee.name;
23425
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23426
- return null;
23427
- };
23428
- const displayNameFromFunctionBinding = (functionNode) => {
23429
- let current = functionNode;
23430
- for (;;) {
23431
- const parent = current.parent;
23432
- if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23433
- const calleeName = hocWrapperCalleeName(parent.callee);
23434
- if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23435
- current = parent;
23436
- continue;
23437
- }
23438
- }
23439
- break;
23440
- }
23441
- const binding = current.parent;
23442
- if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23443
- return null;
23444
- };
23445
- const componentOrHookDisplayNameForFunction = (functionNode) => {
23446
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23447
- return displayNameFromFunctionBinding(functionNode);
23448
- };
23449
- //#endregion
23450
- //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23451
- const enclosingComponentOrHookName = (node) => {
23452
- const functionNode = findEnclosingFunction(node);
23453
- return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23454
- };
23455
- //#endregion
23456
23823
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23457
23824
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23458
23825
  const CONTEXT_MODULES = [
@@ -24423,11 +24790,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24423
24790
  return isInitialOnlyPropName(arg.name);
24424
24791
  };
24425
24792
  //#endregion
24793
+ //#region src/plugin/utils/is-no-op-statement.ts
24794
+ const isNoOpStatement = (statement) => {
24795
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24796
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24797
+ const expression = stripParenExpression(statement.expression);
24798
+ if (isNodeOfType(expression, "Literal")) return true;
24799
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24800
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24801
+ return false;
24802
+ };
24803
+ //#endregion
24426
24804
  //#region src/plugin/utils/get-callback-statements.ts
24427
24805
  const getCallbackStatements = (callback) => {
24428
24806
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
24429
- if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
24430
- return callback.body ? [callback.body] : [];
24807
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24431
24808
  };
24432
24809
  //#endregion
24433
24810
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24466,6 +24843,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24466
24843
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24467
24844
  }
24468
24845
  };
24846
+ const flattenGuardedStatements = (statements) => {
24847
+ const flattened = [];
24848
+ for (const statement of statements) {
24849
+ if (isNoOpStatement(statement)) continue;
24850
+ if (isNodeOfType(statement, "ExpressionStatement")) {
24851
+ flattened.push(statement);
24852
+ continue;
24853
+ }
24854
+ if (isNodeOfType(statement, "IfStatement")) {
24855
+ for (const branch of [statement.consequent, statement.alternate]) {
24856
+ if (!branch) continue;
24857
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24858
+ if (flattenedBranch === null) return null;
24859
+ flattened.push(...flattenedBranch);
24860
+ }
24861
+ continue;
24862
+ }
24863
+ return null;
24864
+ }
24865
+ return flattened;
24866
+ };
24469
24867
  const noDerivedStateEffect = defineRule({
24470
24868
  id: "no-derived-state-effect",
24471
24869
  title: "Derived state stored in an effect",
@@ -24497,8 +24895,8 @@ const noDerivedStateEffect = defineRule({
24497
24895
  }
24498
24896
  }
24499
24897
  if (sawAnyDep && allDepsAreInitialOnly) return;
24500
- const statements = getCallbackStatements(callback);
24501
- if (statements.length === 0) return;
24898
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24899
+ if (statements === null || statements.length === 0) return;
24502
24900
  if (!statements.every((statement) => {
24503
24901
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24504
24902
  const expression = statement.expression;
@@ -25133,7 +25531,7 @@ const noDidMountSetState = defineRule({
25133
25531
  const { mode } = resolveSettings$20(context.settings);
25134
25532
  return { CallExpression(node) {
25135
25533
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25136
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25534
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25137
25535
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25138
25536
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25139
25537
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25258,7 +25656,7 @@ const noDidUpdateSetState = defineRule({
25258
25656
  const { mode } = resolveSettings$19(context.settings);
25259
25657
  return { CallExpression(node) {
25260
25658
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25261
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25659
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25262
25660
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25263
25661
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25264
25662
  if (isInsideDiffGuard(node)) return;
@@ -25381,6 +25779,33 @@ const initializerMarksPlainState = (initializerArgument) => {
25381
25779
  }
25382
25780
  return producesPlainStateValue(unwrapped);
25383
25781
  };
25782
+ const producesOpaqueInstanceValue = (expression) => {
25783
+ if (isNodeOfType(expression, "NewExpression")) return true;
25784
+ return isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "MemberExpression") && !isPlainDataProducerCall(expression);
25785
+ };
25786
+ const collectSetterValueObservations = (componentBody, setterNames) => {
25787
+ const plainFedSetterNames = /* @__PURE__ */ new Set();
25788
+ const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25789
+ walkAst(componentBody, (node) => {
25790
+ if (!isNodeOfType(node, "CallExpression")) return;
25791
+ if (!isNodeOfType(node.callee, "Identifier")) return;
25792
+ const setterName = node.callee.name;
25793
+ if (!setterNames.has(setterName)) return;
25794
+ const argument = node.arguments?.[0];
25795
+ if (!argument) return;
25796
+ const unwrapped = stripParenExpression(argument);
25797
+ if (isNullOrUndefinedExpression(unwrapped)) return;
25798
+ if (producesPlainStateValue(unwrapped)) {
25799
+ plainFedSetterNames.add(setterName);
25800
+ return;
25801
+ }
25802
+ if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25803
+ });
25804
+ return {
25805
+ plainFedSetterNames,
25806
+ opaqueFedSetterNames
25807
+ };
25808
+ };
25384
25809
  const collectCallbackRefSetterNames = (componentBody) => {
25385
25810
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25386
25811
  walkAst(componentBody, (node) => {
@@ -25450,11 +25875,15 @@ const noDirectStateMutation = defineRule({
25450
25875
  if (bindings.length === 0) return;
25451
25876
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25452
25877
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
25878
+ const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25453
25879
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25454
25880
  for (const binding of bindings) {
25455
25881
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25456
25882
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25457
- if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
25883
+ const initializerArgument = binding.declarator.init.arguments?.[0];
25884
+ if (!initializerMarksPlainState(initializerArgument)) continue;
25885
+ if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
25886
+ plainObjectStateValueNames.add(binding.valueName);
25458
25887
  }
25459
25888
  const visitMutationCandidate = (child, currentlyShadowed) => {
25460
25889
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -25579,9 +26008,10 @@ const noDocumentStartViewTransition = defineRule({
25579
26008
  create: (context) => ({ CallExpression(node) {
25580
26009
  const callee = node.callee;
25581
26010
  if (!isNodeOfType(callee, "MemberExpression")) return;
25582
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26011
+ const receiver = stripParenExpression(callee.object);
26012
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25583
26013
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25584
- if (context.scopes.symbolFor(callee.object) !== null) return;
26014
+ if (context.scopes.symbolFor(receiver) !== null) return;
25585
26015
  if (!importsReactViewTransition(node)) return;
25586
26016
  context.report({
25587
26017
  node,
@@ -25601,7 +26031,8 @@ const noDocumentWrite = defineRule({
25601
26031
  create: (context) => ({ CallExpression(node) {
25602
26032
  const callee = node.callee;
25603
26033
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25604
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26034
+ const receiver = stripParenExpression(callee.object);
26035
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25605
26036
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25606
26037
  context.report({
25607
26038
  node,
@@ -26233,13 +26664,6 @@ const noEffectEventHandler = defineRule({
26233
26664
  }
26234
26665
  });
26235
26666
  //#endregion
26236
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
26237
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
26238
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
26239
- if (importSource === null) return false;
26240
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
26241
- };
26242
- //#endregion
26243
26667
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26244
26668
  const createComponentBindingStackTracker = (callbacks) => {
26245
26669
  const componentBindingStack = [];
@@ -26293,7 +26717,7 @@ const noEffectEventInDeps = defineRule({
26293
26717
  const initializer = declaratorNode.init;
26294
26718
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26295
26719
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26296
- if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26720
+ if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26297
26721
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26298
26722
  } });
26299
26723
  return {
@@ -28873,7 +29297,7 @@ const noIsMounted = defineRule({
28873
29297
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28874
29298
  create: (context) => ({ CallExpression(node) {
28875
29299
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28876
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29300
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28877
29301
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28878
29302
  if (!getParentComponent(node)) return;
28879
29303
  context.report({
@@ -28888,7 +29312,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28888
29312
  const isJsonMethodCall = (node, method) => {
28889
29313
  if (!isNodeOfType(node, "CallExpression")) return false;
28890
29314
  const callee = node.callee;
28891
- return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
29315
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29316
+ const receiver = stripParenExpression(callee.object);
29317
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28892
29318
  };
28893
29319
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28894
29320
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -28973,7 +29399,7 @@ const extractReturnTypeAnnotation = (returnType) => {
28973
29399
  const noJsxElementType = defineRule({
28974
29400
  id: "no-jsx-element-type",
28975
29401
  title: "No JSX.Element",
28976
- severity: "error",
29402
+ severity: "warn",
28977
29403
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
28978
29404
  create: (context) => {
28979
29405
  let isJsxImported = false;
@@ -29299,6 +29725,378 @@ const noLegacyContextApi = defineRule({
29299
29725
  }
29300
29726
  });
29301
29727
  //#endregion
29728
+ //#region src/plugin/utils/executes-during-render.ts
29729
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29730
+ "map",
29731
+ "filter",
29732
+ "forEach",
29733
+ "flatMap",
29734
+ "reduce",
29735
+ "reduceRight",
29736
+ "some",
29737
+ "every",
29738
+ "find",
29739
+ "findIndex",
29740
+ "findLast",
29741
+ "findLastIndex",
29742
+ "sort",
29743
+ "toSorted"
29744
+ ]);
29745
+ const executesDuringRender = (functionNode) => {
29746
+ const parent = functionNode.parent;
29747
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29748
+ if (parent.callee === functionNode) return true;
29749
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29750
+ 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;
29751
+ };
29752
+ //#endregion
29753
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29754
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29755
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29756
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29757
+ return false;
29758
+ };
29759
+ //#endregion
29760
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29761
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29762
+ let ancestor = bindingIdentifier.parent;
29763
+ while (ancestor) {
29764
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29765
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29766
+ ancestor = ancestor.parent ?? null;
29767
+ }
29768
+ return null;
29769
+ };
29770
+ //#endregion
29771
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29772
+ const isFalsyLiteral = (node) => {
29773
+ if (!node) return true;
29774
+ if (isNodeOfType(node, "Literal")) return !node.value;
29775
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29776
+ };
29777
+ const isFalsyInitialStateBinding = (identifier) => {
29778
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29779
+ const binding = findVariableInitializer(identifier, identifier.name);
29780
+ if (!binding) return false;
29781
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29782
+ if (!declarator?.init) return false;
29783
+ const init = stripParenExpression(declarator.init);
29784
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29785
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29786
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29787
+ return isFalsyLiteral(init.arguments?.[0]);
29788
+ };
29789
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29790
+ //#endregion
29791
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29792
+ const isGatedByFalsyInitialState = (node) => {
29793
+ let cursor = node;
29794
+ let parent = node.parent;
29795
+ while (parent) {
29796
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29797
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29798
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29799
+ cursor = parent;
29800
+ parent = parent.parent ?? null;
29801
+ }
29802
+ return false;
29803
+ };
29804
+ //#endregion
29805
+ //#region src/plugin/utils/references-client-only-flag.ts
29806
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29807
+ const referencesClientOnlyFlag = (expression) => {
29808
+ const unwrapped = stripParenExpression(expression);
29809
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29810
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29811
+ const property = unwrapped.property;
29812
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29813
+ }
29814
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29815
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29816
+ return false;
29817
+ };
29818
+ //#endregion
29819
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29820
+ const isInsideClientOnlyGuard = (node) => {
29821
+ let cursor = node;
29822
+ let parent = node.parent;
29823
+ while (parent) {
29824
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29825
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29826
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29827
+ cursor = parent;
29828
+ parent = parent.parent ?? null;
29829
+ }
29830
+ return false;
29831
+ };
29832
+ //#endregion
29833
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29834
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29835
+ "toLocaleString",
29836
+ "toLocaleDateString",
29837
+ "toLocaleTimeString"
29838
+ ]);
29839
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29840
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29841
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29842
+ "format",
29843
+ "formatToParts",
29844
+ "formatRange"
29845
+ ]);
29846
+ const isProvableDateExpression = (expression) => {
29847
+ if (!expression) return false;
29848
+ const unwrapped = stripParenExpression(expression);
29849
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29850
+ };
29851
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29852
+ const receiverNameLooksDateFlavored = (expression) => {
29853
+ if (!expression) return false;
29854
+ const unwrapped = stripParenExpression(expression);
29855
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29856
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29857
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29858
+ return false;
29859
+ };
29860
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29861
+ if (!objectExpression) return false;
29862
+ const unwrapped = stripParenExpression(objectExpression);
29863
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29864
+ for (const property of unwrapped.properties ?? []) {
29865
+ if (!isNodeOfType(property, "Property")) continue;
29866
+ if (property.computed) continue;
29867
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29868
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29869
+ }
29870
+ return false;
29871
+ };
29872
+ const hasExplicitLocaleArgument = (argument) => {
29873
+ if (!argument) return false;
29874
+ const unwrapped = stripParenExpression(argument);
29875
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29876
+ return true;
29877
+ };
29878
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29879
+ const localeArgument = call.arguments?.[0];
29880
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29881
+ const optionsArgument = call.arguments?.[1];
29882
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29883
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29884
+ };
29885
+ const matchLocaleMethodCall = (call) => {
29886
+ const callee = call.callee;
29887
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29888
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29889
+ const methodName = callee.property.name;
29890
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29891
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29892
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29893
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29894
+ return {
29895
+ node: call,
29896
+ display: `${methodName}()`
29897
+ };
29898
+ };
29899
+ const getIntlFormatterName = (expression) => {
29900
+ if (!expression) return null;
29901
+ const unwrapped = stripParenExpression(expression);
29902
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29903
+ const callee = unwrapped.callee;
29904
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29905
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29906
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29907
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29908
+ };
29909
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29910
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29911
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29912
+ if (formatterName !== "DateTimeFormat") return true;
29913
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29914
+ };
29915
+ const matchIntlFormatCall = (call) => {
29916
+ const callee = call.callee;
29917
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29918
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29919
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29920
+ let construction = stripParenExpression(callee.object);
29921
+ if (isNodeOfType(construction, "Identifier")) {
29922
+ const binding = findVariableInitializer(construction, construction.name);
29923
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29924
+ }
29925
+ if (!construction) return null;
29926
+ const formatterName = getIntlFormatterName(construction);
29927
+ if (!formatterName) return null;
29928
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29929
+ return {
29930
+ node: call,
29931
+ display: `Intl.${formatterName}().${callee.property.name}()`
29932
+ };
29933
+ };
29934
+ const isDeterministicInputDateConstruction = (expression) => {
29935
+ if (!expression) return false;
29936
+ const unwrapped = stripParenExpression(expression);
29937
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29938
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29939
+ return (unwrapped.arguments?.length ?? 0) > 0;
29940
+ };
29941
+ const matchDateDefaultStringification = (node) => {
29942
+ if (isNodeOfType(node, "CallExpression")) {
29943
+ const callee = node.callee;
29944
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29945
+ node,
29946
+ display: "Date.prototype.toString()"
29947
+ };
29948
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29949
+ node,
29950
+ display: "String(new Date(…))"
29951
+ };
29952
+ return null;
29953
+ }
29954
+ if (isNodeOfType(node, "TemplateLiteral")) {
29955
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29956
+ node: expression,
29957
+ display: "`${new Date(…)}`"
29958
+ };
29959
+ }
29960
+ return null;
29961
+ };
29962
+ const findRenderPhaseComponentOrHook = (node) => {
29963
+ let functionNode = findEnclosingFunction(node);
29964
+ while (functionNode) {
29965
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29966
+ if (!executesDuringRender(functionNode)) return null;
29967
+ functionNode = findEnclosingFunction(functionNode);
29968
+ }
29969
+ return null;
29970
+ };
29971
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29972
+ if (fileHasUseClientDirective) return true;
29973
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
29974
+ if (displayName && isReactHookName(displayName)) return true;
29975
+ let callsHook = false;
29976
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
29977
+ if (callsHook) return false;
29978
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
29979
+ callsHook = true;
29980
+ return false;
29981
+ }
29982
+ });
29983
+ return callsHook;
29984
+ };
29985
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
29986
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
29987
+ if (!isNodeOfType(body, "BlockStatement")) return false;
29988
+ const ancestors = /* @__PURE__ */ new Set();
29989
+ let cursor = node;
29990
+ while (cursor) {
29991
+ ancestors.add(cursor);
29992
+ cursor = cursor.parent ?? null;
29993
+ }
29994
+ for (const statement of body.body ?? []) {
29995
+ if (ancestors.has(statement)) return false;
29996
+ if (!isNodeOfType(statement, "IfStatement")) continue;
29997
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
29998
+ let returnsEarly = false;
29999
+ walkAst(statement.consequent, (child) => {
30000
+ if (isFunctionLike$1(child)) return false;
30001
+ if (isNodeOfType(child, "ReturnStatement")) {
30002
+ returnsEarly = true;
30003
+ return false;
30004
+ }
30005
+ });
30006
+ if (returnsEarly) return true;
30007
+ }
30008
+ return false;
30009
+ };
30010
+ const findEnclosingJsxOpeningElement = (node) => {
30011
+ let cursor = node.parent;
30012
+ while (cursor) {
30013
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30014
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30015
+ cursor = cursor.parent ?? null;
30016
+ }
30017
+ return null;
30018
+ };
30019
+ const noLocaleFormatInRender = defineRule({
30020
+ id: "no-locale-format-in-render",
30021
+ title: "Locale/timezone formatting during render",
30022
+ severity: "warn",
30023
+ category: "Correctness",
30024
+ disabledWhen: [
30025
+ "vite",
30026
+ "cra",
30027
+ "expo",
30028
+ "react-native",
30029
+ "unknown"
30030
+ ],
30031
+ 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.",
30032
+ create: (context) => {
30033
+ if (isTestlikeFilename(context.filename)) return {};
30034
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
30035
+ let fileHasUseClientDirective = false;
30036
+ let fileIsEmailTemplate = false;
30037
+ const reportedNodes = /* @__PURE__ */ new Set();
30038
+ const reportIfRenderPhase = (match) => {
30039
+ if (reportedNodes.has(match.node)) return;
30040
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30041
+ if (!componentOrHookNode) return;
30042
+ if (fileIsEmailTemplate) return;
30043
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30044
+ if (isInsideClientOnlyGuard(match.node)) return;
30045
+ if (isGatedByFalsyInitialState(match.node)) return;
30046
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30047
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30048
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30049
+ reportedNodes.add(match.node);
30050
+ context.report({
30051
+ node: match.node,
30052
+ 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.`
30053
+ });
30054
+ };
30055
+ return {
30056
+ Program(node) {
30057
+ fileHasUseClientDirective = hasDirective(node, "use client");
30058
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30059
+ },
30060
+ CallExpression(node) {
30061
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30062
+ if (match) reportIfRenderPhase(match);
30063
+ },
30064
+ TemplateLiteral(node) {
30065
+ const match = matchDateDefaultStringification(node);
30066
+ if (match) reportIfRenderPhase(match);
30067
+ },
30068
+ JSXExpressionContainer(node) {
30069
+ const expression = stripParenExpression(node.expression);
30070
+ if (!isNodeOfType(expression, "CallExpression")) return;
30071
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30072
+ const helperName = expression.callee.name;
30073
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30074
+ if (!componentOrHookNode) return;
30075
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30076
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30077
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30078
+ walkAst(helperNode.body ?? helperNode, (child) => {
30079
+ if (isFunctionLike$1(child)) return false;
30080
+ if (!isNodeOfType(child, "CallExpression")) return;
30081
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30082
+ if (!match || reportedNodes.has(match.node)) return;
30083
+ if (fileIsEmailTemplate) return;
30084
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30085
+ if (isInsideClientOnlyGuard(node)) return;
30086
+ if (isGatedByFalsyInitialState(node)) return;
30087
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30088
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30089
+ reportedNodes.add(match.node);
30090
+ context.report({
30091
+ node: match.node,
30092
+ 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.`
30093
+ });
30094
+ });
30095
+ }
30096
+ };
30097
+ }
30098
+ });
30099
+ //#endregion
29302
30100
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29303
30101
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29304
30102
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30112,7 +30910,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30112
30910
  "reverse",
30113
30911
  "sort"
30114
30912
  ]);
30115
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30116
30913
  const OBJECT_MUTATION_METHODS = new Set([
30117
30914
  "assign",
30118
30915
  "defineProperties",
@@ -30186,7 +30983,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30186
30983
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30187
30984
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30188
30985
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30189
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30190
30986
  }
30191
30987
  }
30192
30988
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30225,6 +31021,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30225
31021
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30226
31022
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30227
31023
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
31024
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30228
31025
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30229
31026
  });
30230
31027
  return mutations;
@@ -30457,7 +31254,7 @@ const noNestedComponentDefinition = defineRule({
30457
31254
  id: "no-nested-component-definition",
30458
31255
  title: "Component defined inside another component",
30459
31256
  tags: ["test-noise", "react-jsx-only"],
30460
- severity: "error",
31257
+ severity: "warn",
30461
31258
  category: "Correctness",
30462
31259
  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.",
30463
31260
  create: (context) => {
@@ -31412,12 +32209,12 @@ const noPassDataToParent = defineRule({
31412
32209
  if (calleeNode === identifier) {
31413
32210
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31414
32211
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31415
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32212
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31416
32213
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31417
32214
  if (isCustomHookParameter(ref)) continue;
31418
32215
  } else continue;
31419
32216
  const methodName = getCallMethodName(calleeNode);
31420
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32217
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31421
32218
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31422
32219
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31423
32220
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31608,7 +32405,7 @@ const noPassLiveStateToParent = defineRule({
31608
32405
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31609
32406
  const calleeNode = callExpr.callee;
31610
32407
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31611
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32408
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31612
32409
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31613
32410
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31614
32411
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -31936,40 +32733,6 @@ const noPreventDefault = defineRule({
31936
32733
  }
31937
32734
  });
31938
32735
  //#endregion
31939
- //#region src/plugin/utils/is-result-discarded-call.ts
31940
- const isResultDiscardedCall = (callExpression) => {
31941
- let node = callExpression;
31942
- let parent = node.parent;
31943
- while (parent) {
31944
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
31945
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31946
- if (isNodeOfType(parent, "ChainExpression")) {
31947
- node = parent;
31948
- parent = node.parent;
31949
- continue;
31950
- }
31951
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31952
- node = parent;
31953
- parent = node.parent;
31954
- continue;
31955
- }
31956
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31957
- node = parent;
31958
- parent = node.parent;
31959
- continue;
31960
- }
31961
- if (isNodeOfType(parent, "SequenceExpression")) {
31962
- const expressions = parent.expressions ?? [];
31963
- if (expressions[expressions.length - 1] !== node) return true;
31964
- node = parent;
31965
- parent = node.parent;
31966
- continue;
31967
- }
31968
- return false;
31969
- }
31970
- return false;
31971
- };
31972
- //#endregion
31973
32736
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
31974
32737
  const isRefLatchGuardedEffect = (callbackBody) => {
31975
32738
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32176,7 +32939,7 @@ const isAlwaysFreshExpression = (expression) => {
32176
32939
  return `${callee.name}()`;
32177
32940
  }
32178
32941
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32179
- const receiver = callee.object;
32942
+ const receiver = stripParenExpression(callee.object);
32180
32943
  const property = callee.property;
32181
32944
  if (!isNodeOfType(property, "Identifier")) return null;
32182
32945
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32297,8 +33060,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32297
33060
  if (typeof sourceValue !== "string") return;
32298
33061
  if (handleExtraSource?.(node, context)) return;
32299
33062
  if (sourceValue !== source) return;
33063
+ if (isTypeOnlyImport(node)) return;
32300
33064
  for (const specifier of node.specifiers ?? []) {
32301
33065
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33066
+ if (specifier.importKind === "type") continue;
32302
33067
  const importedName = getImportedName$1(specifier);
32303
33068
  if (!importedName) continue;
32304
33069
  const message = messages.get(importedName);
@@ -32317,8 +33082,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32317
33082
  MemberExpression(node) {
32318
33083
  if (namespaceBindings.size === 0) return;
32319
33084
  if (node.computed) return;
32320
- if (!isNodeOfType(node.object, "Identifier")) return;
32321
- if (!namespaceBindings.has(node.object.name)) return;
33085
+ const receiver = stripParenExpression(node.object);
33086
+ if (!isNodeOfType(receiver, "Identifier")) return;
33087
+ if (!namespaceBindings.has(receiver.name)) return;
32322
33088
  if (!isNodeOfType(node.property, "Identifier")) return;
32323
33089
  const message = messages.get(node.property.name);
32324
33090
  if (message) context.report({
@@ -32383,7 +33149,7 @@ const noReactDomDeprecatedApis = defineRule({
32383
33149
  });
32384
33150
  //#endregion
32385
33151
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32386
- 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'`."]]);
33152
+ 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."]]);
32387
33153
  const isVendoredShadcnUiFilename = (rawFilename) => {
32388
33154
  if (!rawFilename) return false;
32389
33155
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32421,7 +33187,7 @@ const noReact19DeprecatedApis = defineRule({
32421
33187
  requires: ["react:19"],
32422
33188
  tags: ["test-noise", "migration-hint"],
32423
33189
  severity: "warn",
32424
- 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.",
33190
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32425
33191
  create: (context) => {
32426
33192
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32427
33193
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32745,34 +33511,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32745
33511
  });
32746
33512
  //#endregion
32747
33513
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32748
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32749
- if (!symbol || visitedSymbols.has(symbol)) return false;
32750
- visitedSymbols.add(symbol);
32751
- if (isComponentParameterSymbol(symbol)) return true;
32752
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32753
- const source = symbol.initializer;
32754
- if (!source) return false;
32755
- return initializerRootsInProps(source, scopes, visitedSymbols);
32756
- };
32757
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32758
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32759
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32760
- return rootsInProps(node, scopes, visitedSymbols);
32761
- };
32762
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32763
- let current = node;
32764
- while (isNodeOfType(current, "MemberExpression")) {
32765
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32766
- current = current.object;
32767
- }
32768
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32769
- return false;
32770
- };
32771
33514
  const isInsideComponentContext = (node) => {
32772
33515
  let cursor = node.parent;
32773
33516
  while (cursor) {
32774
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32775
33517
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33518
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32776
33519
  cursor = cursor.parent ?? null;
32777
33520
  }
32778
33521
  return false;
@@ -32782,24 +33525,28 @@ const functionBodyOf = (node) => {
32782
33525
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32783
33526
  return null;
32784
33527
  };
33528
+ const isHookCallee = (callee) => {
33529
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33530
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33531
+ return false;
33532
+ };
32785
33533
  const containsHookCall = (body) => {
32786
33534
  let found = false;
32787
33535
  walkAst(body, (child) => {
32788
- if (found) return;
33536
+ if (found) return false;
33537
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32789
33538
  if (!isNodeOfType(child, "CallExpression")) return;
32790
- const name = getCalleeName$2(child);
32791
- if (name && isReactHookName(name)) found = true;
33539
+ if (isHookCallee(child.callee)) found = true;
32792
33540
  });
32793
33541
  return found;
32794
33542
  };
32795
- const isModuleScopeHookFreeHelper = (symbol) => {
33543
+ const isHookCallingRenderHelper = (symbol) => {
32796
33544
  if (!symbol) return false;
32797
33545
  const declaration = symbol.declarationNode;
32798
33546
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32799
33547
  const body = functionBodyOf(declaration);
32800
33548
  if (!body) return false;
32801
- if (findEnclosingFunction(declaration) !== null) return false;
32802
- return !containsHookCall(body);
33549
+ return containsHookCall(body);
32803
33550
  };
32804
33551
  const noRenderInRender = defineRule({
32805
33552
  id: "no-render-in-render",
@@ -32808,20 +33555,13 @@ const noRenderInRender = defineRule({
32808
33555
  tags: ["test-noise"],
32809
33556
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32810
33557
  create: (context) => ({ JSXExpressionContainer(node) {
32811
- const expression = node.expression;
33558
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32812
33559
  if (!isNodeOfType(expression, "CallExpression")) return;
32813
- let calleeName = null;
32814
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32815
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32816
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33560
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33561
+ const calleeName = expression.callee.name;
33562
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32817
33563
  if (!isInsideComponentContext(node)) return;
32818
- if (isNodeOfType(expression.callee, "Identifier")) {
32819
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
32820
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32821
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32822
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32823
- if (rootsInProps(expression.callee.object, context.scopes)) return;
32824
- }
33564
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32825
33565
  context.report({
32826
33566
  node: expression,
32827
33567
  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.`
@@ -32882,13 +33622,7 @@ const noRenderPropChildren = defineRule({
32882
33622
  //#endregion
32883
33623
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32884
33624
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
32885
- const isReactDomRenderCall = (node) => {
32886
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32887
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32888
- if (node.callee.object.name !== "ReactDOM") return false;
32889
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32890
- return node.callee.property.name === "render";
32891
- };
33625
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32892
33626
  const isUsedAsReturnValue = (parent) => {
32893
33627
  if (!parent) return false;
32894
33628
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33724,7 +34458,7 @@ const noSetState = defineRule({
33724
34458
  category: "Architecture",
33725
34459
  create: (context) => ({ CallExpression(node) {
33726
34460
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33727
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34461
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33728
34462
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33729
34463
  if (!getParentComponent(node)) return;
33730
34464
  context.report({
@@ -36170,6 +36904,36 @@ const isTriviallyCheapExpression = (node) => {
36170
36904
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36171
36905
  return true;
36172
36906
  };
36907
+ const isTrivialContainerLiteral = (node) => {
36908
+ if (!node) return false;
36909
+ const innerExpression = stripParenExpression(node);
36910
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36911
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36912
+ return false;
36913
+ };
36914
+ const isNonEscapingRead = (identifier) => {
36915
+ const readRoot = findTransparentExpressionRoot(identifier);
36916
+ const memberNode = readRoot.parent;
36917
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
36918
+ const memberUse = findTransparentExpressionRoot(memberNode);
36919
+ const memberUseParent = memberUse.parent;
36920
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
36921
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
36922
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
36923
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
36924
+ };
36925
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
36926
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
36927
+ const parentNode = memoUsageRoot.parent;
36928
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36929
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
36930
+ const bindingTarget = parentNode.id;
36931
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36932
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36933
+ const symbol = scopes.symbolFor(bindingTarget);
36934
+ if (!symbol) return false;
36935
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36936
+ };
36173
36937
  const noUsememoSimpleExpression = defineRule({
36174
36938
  id: "no-usememo-simple-expression",
36175
36939
  title: "useMemo on a cheap value",
@@ -36191,9 +36955,17 @@ const noUsememoSimpleExpression = defineRule({
36191
36955
  let returnExpression = null;
36192
36956
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36193
36957
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36194
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
36958
+ if (!returnExpression) return;
36959
+ if (isTriviallyCheapExpression(returnExpression)) {
36960
+ context.report({
36961
+ node,
36962
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36963
+ });
36964
+ return;
36965
+ }
36966
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36195
36967
  node,
36196
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36968
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36197
36969
  });
36198
36970
  } })
36199
36971
  });
@@ -36299,7 +37071,7 @@ const noWillUpdateSetState = defineRule({
36299
37071
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36300
37072
  return { CallExpression(node) {
36301
37073
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36302
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37074
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36303
37075
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36304
37076
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36305
37077
  context.report({
@@ -36575,8 +37347,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36575
37347
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36576
37348
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36577
37349
  const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
36578
- const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
36579
- const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
37350
+ const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
36580
37351
  const DEFAULT_REACT_HOCS = [
36581
37352
  "memo",
36582
37353
  "forwardRef",
@@ -36617,6 +37388,10 @@ const isRouteFactoryCall = (expression) => {
36617
37388
  }
36618
37389
  return false;
36619
37390
  };
37391
+ const isConfigOnlyFactoryCall = (call) => call.arguments.every((argument) => {
37392
+ const expression = skipTsExpression(argument);
37393
+ return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37394
+ });
36620
37395
  const isReactHocName = (name, state) => state.customHocs.has(name);
36621
37396
  const isHocCallee = (callee, state) => {
36622
37397
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -36647,6 +37422,20 @@ const isReactComponentInitializer = (expression, state) => {
36647
37422
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36648
37423
  return false;
36649
37424
  };
37425
+ const objectExpressionBundlesComponents = (objectExpression, state) => {
37426
+ for (const property of objectExpression.properties ?? []) {
37427
+ if (!isNodeOfType(property, "Property")) continue;
37428
+ const value = skipTsExpression(property.value);
37429
+ if (isNodeOfType(value, "Identifier")) {
37430
+ if (state.localComponentNames.has(value.name)) return true;
37431
+ continue;
37432
+ }
37433
+ if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
37434
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
37435
+ if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
37436
+ }
37437
+ return false;
37438
+ };
36650
37439
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36651
37440
  if (initializer) {
36652
37441
  const expression = skipTsExpression(initializer);
@@ -36677,6 +37466,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36677
37466
  reportNode
36678
37467
  };
36679
37468
  }
37469
+ if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
37470
+ kind: "namespace-object",
37471
+ reportNode
37472
+ };
36680
37473
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36681
37474
  kind: "non-component",
36682
37475
  reportNode
@@ -36693,7 +37486,7 @@ const collectRelevantNodes = (programRoot) => {
36693
37486
  walkAst(programRoot, (child) => {
36694
37487
  const childType = child.type;
36695
37488
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36696
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
37489
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36697
37490
  });
36698
37491
  return {
36699
37492
  exportNodes,
@@ -36738,18 +37531,35 @@ const onlyExportComponents = defineRule({
36738
37531
  category: "Architecture",
36739
37532
  create: (context) => {
36740
37533
  const settings = resolveSettings$6(context.settings);
36741
- const state = {
36742
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36743
- allowExportNames: new Set(settings.allowExportNames),
36744
- allowConstantExport: settings.allowConstantExport
36745
- };
36746
37534
  return { Program(node) {
36747
37535
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
36748
37536
  const { exportNodes, componentCandidates } = collectRelevantNodes(node);
37537
+ const localComponentNames = /* @__PURE__ */ new Set();
37538
+ const state = {
37539
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
37540
+ allowExportNames: new Set(settings.allowExportNames),
37541
+ allowConstantExport: settings.allowConstantExport,
37542
+ localComponentNames,
37543
+ scopes: context.scopes
37544
+ };
37545
+ for (const child of componentCandidates) {
37546
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
37547
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
37548
+ }
37549
+ if (isNodeOfType(child, "ClassDeclaration") && child.id) {
37550
+ if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37551
+ }
37552
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
37553
+ const initializer = child.init;
37554
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37555
+ const expression = initializer ? skipTsExpression(initializer) : null;
37556
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37557
+ }
37558
+ }
37559
+ }
36749
37560
  const exports = [];
36750
37561
  let hasReactExport = false;
36751
37562
  let hasAnyExports = false;
36752
- const localComponents = [];
36753
37563
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
36754
37564
  for (const child of exportNodes) {
36755
37565
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -36814,13 +37624,24 @@ const onlyExportComponents = defineRule({
36814
37624
  return false;
36815
37625
  })();
36816
37626
  if (isHoc && firstArgIsValid) hasReactExport = true;
37627
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
37628
+ kind: "non-component",
37629
+ reportNode: stripped
37630
+ });
36817
37631
  else context.report({
36818
37632
  node: stripped,
36819
37633
  message: ANONYMOUS_MESSAGE
36820
37634
  });
36821
37635
  continue;
36822
37636
  }
36823
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
37637
+ if (isNodeOfType(stripped, "ObjectExpression")) {
37638
+ context.report({
37639
+ node: stripped,
37640
+ message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
37641
+ });
37642
+ continue;
37643
+ }
37644
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
36824
37645
  context.report({
36825
37646
  node: stripped,
36826
37647
  message: ANONYMOUS_MESSAGE
@@ -36873,32 +37694,23 @@ const onlyExportComponents = defineRule({
36873
37694
  let entry;
36874
37695
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
36875
37696
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
36876
- else entry = {
36877
- kind: "non-component",
36878
- reportNode
36879
- };
37697
+ else {
37698
+ entry = {
37699
+ kind: "non-component",
37700
+ reportNode
37701
+ };
37702
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
37703
+ }
36880
37704
  if (isReExportFromSource && entry.kind !== "react-component") continue;
36881
37705
  exports.push(entry);
36882
37706
  }
36883
37707
  }
36884
37708
  }
36885
37709
  for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
36886
- const isInsideExport = (node) => {
36887
- let walker = node.parent;
36888
- while (walker) {
36889
- if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
36890
- walker = walker.parent ?? null;
36891
- }
36892
- return false;
36893
- };
36894
- for (const child of componentCandidates) {
36895
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36896
- if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36897
- }
36898
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36899
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36900
- }
36901
- }
37710
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
37711
+ node: entry.reportNode,
37712
+ message: NAMESPACE_OBJECT_MESSAGE
37713
+ });
36902
37714
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
36903
37715
  if (entry.kind === "non-component") context.report({
36904
37716
  node: entry.reportNode,
@@ -36909,14 +37721,6 @@ const onlyExportComponents = defineRule({
36909
37721
  message: REACT_CONTEXT_MESSAGE
36910
37722
  });
36911
37723
  }
36912
- else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
36913
- node: local,
36914
- message: LOCAL_COMPONENT_MESSAGE
36915
- });
36916
- else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
36917
- node: local,
36918
- message: NO_EXPORT_MESSAGE
36919
- });
36920
37724
  } };
36921
37725
  }
36922
37726
  });
@@ -37714,8 +38518,8 @@ const preferHtmlDialog = defineRule({
37714
38518
  if (!HTML_TAGS.has(tagName)) return;
37715
38519
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37716
38520
  if (roleAttribute) {
37717
- const roleValue = getJsxPropStringValue(roleAttribute);
37718
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
38521
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
38522
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37719
38523
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37720
38524
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37721
38525
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -38419,11 +39223,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38419
39223
  }
38420
39224
  return null;
38421
39225
  };
39226
+ const isTrivialLiteralExpression = (expression) => {
39227
+ if (isNodeOfType(expression, "Literal")) return true;
39228
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39229
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39230
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39231
+ return false;
39232
+ };
38422
39233
  const getSingleSetterCallFromHandler = (handler) => {
38423
39234
  const handlerStatements = getCallbackStatements(handler);
38424
39235
  if (handlerStatements.length !== 1) return null;
38425
39236
  const onlyStatement = handlerStatements[0];
38426
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39237
+ let expression = onlyStatement;
39238
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39239
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39240
+ if (!expression) return null;
39241
+ expression = stripParenExpression(expression);
38427
39242
  if (!isNodeOfType(expression, "CallExpression")) return null;
38428
39243
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38429
39244
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38433,6 +39248,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38433
39248
  setterArgument: expression.arguments[0]
38434
39249
  };
38435
39250
  };
39251
+ const isListenerCollectionInitializer = (init) => {
39252
+ if (!init) return false;
39253
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39254
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39255
+ };
39256
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39257
+ if (!isFunctionLike$1(functionNode)) return false;
39258
+ const firstParam = functionNode.params?.[0];
39259
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39260
+ const listenerParamName = firstParam.name;
39261
+ let registersListener = false;
39262
+ walkAst(functionNode.body, (child) => {
39263
+ if (registersListener) return false;
39264
+ if (!isNodeOfType(child, "CallExpression")) return;
39265
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39266
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39267
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39268
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39269
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39270
+ const registeredArgument = child.arguments?.[0];
39271
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39272
+ });
39273
+ return registersListener;
39274
+ };
39275
+ const buildModuleScopeStoreIndex = (programRoot) => {
39276
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39277
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39278
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39279
+ if (!isNodeOfType(programRoot, "Program")) return {
39280
+ mutableBindingNames,
39281
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39282
+ };
39283
+ for (const statement of programRoot.body ?? []) {
39284
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39285
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39286
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39287
+ continue;
39288
+ }
39289
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39290
+ for (const declarator of unwrapped.declarations ?? []) {
39291
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39292
+ const init = declarator.init ?? null;
39293
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39294
+ mutableBindingNames.add(declarator.id.name);
39295
+ continue;
39296
+ }
39297
+ if (isListenerCollectionInitializer(init)) {
39298
+ listenerCollectionNames.add(declarator.id.name);
39299
+ continue;
39300
+ }
39301
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39302
+ }
39303
+ }
39304
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39305
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39306
+ return {
39307
+ mutableBindingNames,
39308
+ subscribeFunctionNames
39309
+ };
39310
+ };
39311
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39312
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39313
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39314
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39315
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39316
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39317
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39318
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39319
+ return initialArgument.name;
39320
+ };
39321
+ const argumentForwardsSetter = (argument, setterName) => {
39322
+ if (!argument) return false;
39323
+ const unwrapped = stripParenExpression(argument);
39324
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39325
+ if (!isFunctionLike$1(unwrapped)) return false;
39326
+ let callsSetter = false;
39327
+ walkAst(unwrapped.body, (child) => {
39328
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39329
+ callsSetter = true;
39330
+ return false;
39331
+ }
39332
+ });
39333
+ return callsSetter;
39334
+ };
39335
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39336
+ let matchedCall = null;
39337
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39338
+ if (matchedCall) return false;
39339
+ if (!isNodeOfType(child, "CallExpression")) return;
39340
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39341
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39342
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39343
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39344
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39345
+ matchedCall = child;
39346
+ return false;
39347
+ }
39348
+ });
39349
+ return matchedCall;
39350
+ };
38436
39351
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38437
39352
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38438
39353
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38449,6 +39364,16 @@ const preferUseSyncExternalStore = defineRule({
38449
39364
  severity: "warn",
38450
39365
  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.",
38451
39366
  create: (context) => {
39367
+ let cachedStoreIndex = null;
39368
+ const storeIndexFor = (node) => {
39369
+ if (cachedStoreIndex) return cachedStoreIndex;
39370
+ const programRoot = findProgramRoot(node);
39371
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39372
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39373
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39374
+ };
39375
+ return cachedStoreIndex;
39376
+ };
38452
39377
  const checkComponent = (componentBody) => {
38453
39378
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38454
39379
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38486,6 +39411,7 @@ const preferUseSyncExternalStore = defineRule({
38486
39411
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38487
39412
  if (!useStateInitializer) continue;
38488
39413
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39414
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38489
39415
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38490
39416
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38491
39417
  context.report({
@@ -38493,14 +39419,47 @@ const preferUseSyncExternalStore = defineRule({
38493
39419
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38494
39420
  });
38495
39421
  }
39422
+ checkModuleStoreShape(componentBody, useStateBindings);
39423
+ };
39424
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39425
+ const storeIndex = storeIndexFor(componentBody);
39426
+ if (storeIndex.mutableBindingNames.size === 0) return;
39427
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39428
+ const snapshotBindings = useStateBindings.map((binding) => ({
39429
+ binding,
39430
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39431
+ })).filter((candidate) => candidate.storeName !== null);
39432
+ if (snapshotBindings.length === 0) return;
39433
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39434
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39435
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39436
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39437
+ const depsNode = effectCall.arguments[1];
39438
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39439
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39440
+ const callback = getEffectCallback(effectCall);
39441
+ if (!callback || !isFunctionLike$1(callback)) continue;
39442
+ for (const { binding, storeName } of snapshotBindings) {
39443
+ if (reportedDeclarators.has(binding.declarator)) continue;
39444
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39445
+ reportedDeclarators.add(binding.declarator);
39446
+ context.report({
39447
+ node: binding.declarator,
39448
+ 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.`
39449
+ });
39450
+ }
39451
+ }
38496
39452
  };
38497
39453
  return {
38498
39454
  FunctionDeclaration(node) {
38499
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39455
+ const functionName = node.id?.name;
39456
+ if (!functionName) return;
39457
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38500
39458
  checkComponent(node.body);
38501
39459
  },
38502
39460
  VariableDeclarator(node) {
38503
- if (!isComponentAssignment(node)) return;
39461
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39462
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38504
39463
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38505
39464
  checkComponent(node.init.body);
38506
39465
  }
@@ -39110,7 +40069,7 @@ const queryNoVoidQueryFn = defineRule({
39110
40069
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39111
40070
  const body = queryFnValue.body;
39112
40071
  if (!isNodeOfType(body, "BlockStatement")) return;
39113
- if ((body.body ?? []).length === 0) context.report({
40072
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39114
40073
  node: queryFnProperty,
39115
40074
  message: "This empty queryFn caches undefined, so the component never gets data."
39116
40075
  });
@@ -39617,17 +40576,6 @@ const renderingHoistJsx = defineRule({
39617
40576
  }
39618
40577
  });
39619
40578
  //#endregion
39620
- //#region src/plugin/utils/find-declarator-for-binding.ts
39621
- const findDeclaratorForBinding = (bindingIdentifier) => {
39622
- let ancestor = bindingIdentifier.parent;
39623
- while (ancestor) {
39624
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39625
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39626
- ancestor = ancestor.parent ?? null;
39627
- }
39628
- return null;
39629
- };
39630
- //#endregion
39631
40579
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39632
40580
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39633
40581
  {
@@ -39636,19 +40584,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39636
40584
  },
39637
40585
  {
39638
40586
  display: "Date.now()",
39639
- 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"
40587
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39640
40588
  },
39641
40589
  {
39642
40590
  display: "Math.random()",
39643
- 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"
40591
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39644
40592
  },
39645
40593
  {
39646
40594
  display: "performance.now()",
39647
- 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"
40595
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39648
40596
  },
39649
40597
  {
39650
40598
  display: "crypto.randomUUID()",
39651
- 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"
40599
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39652
40600
  }
39653
40601
  ];
39654
40602
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39660,54 +40608,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39660
40608
  }
39661
40609
  return null;
39662
40610
  };
39663
- const executesDuringRender = (functionNode) => {
39664
- const parent = functionNode.parent;
39665
- if (!isNodeOfType(parent, "CallExpression")) return false;
39666
- if (parent.callee === functionNode) return true;
39667
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39668
- };
39669
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39670
- const referencesClientOnlyFlag = (expression) => {
39671
- const unwrapped = stripParenExpression(expression);
39672
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39673
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39674
- const property = unwrapped.property;
39675
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39676
- }
39677
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39678
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39679
- return false;
39680
- };
39681
- const isFalsyLiteral = (node) => {
39682
- if (!node) return true;
39683
- if (isNodeOfType(node, "Literal")) return !node.value;
39684
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39685
- };
39686
- const isFalsyInitialStateBinding = (identifier) => {
39687
- if (!isNodeOfType(identifier, "Identifier")) return false;
39688
- const binding = findVariableInitializer(identifier, identifier.name);
39689
- if (!binding) return false;
39690
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39691
- if (!declarator?.init) return false;
39692
- const init = stripParenExpression(declarator.init);
39693
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39694
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39695
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39696
- return isFalsyLiteral(init.arguments?.[0]);
39697
- };
39698
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39699
- const isGatedByFalsyInitialState = (node) => {
39700
- let cursor = node;
39701
- let parent = node.parent;
39702
- while (parent) {
39703
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39704
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39705
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39706
- cursor = parent;
39707
- parent = parent.parent ?? null;
39708
- }
39709
- return false;
39710
- };
39711
40611
  const isYearOnlyDateRead = (dateNode) => {
39712
40612
  const member = dateNode.parent;
39713
40613
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39731,23 +40631,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39731
40631
  }
39732
40632
  return false;
39733
40633
  };
39734
- const isInsideClientOnlyGuard = (node) => {
39735
- let cursor = node;
39736
- let parent = node.parent;
39737
- while (parent) {
39738
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39739
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39740
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39741
- cursor = parent;
39742
- parent = parent.parent ?? null;
39743
- }
39744
- return false;
39745
- };
39746
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
39747
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39748
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39749
- return false;
39750
- };
39751
40634
  const renderingHydrationMismatchTime = defineRule({
39752
40635
  id: "rendering-hydration-mismatch-time",
39753
40636
  title: "Time or random value in JSX",
@@ -39795,6 +40678,35 @@ const renderingHydrationMismatchTime = defineRule({
39795
40678
  }
39796
40679
  });
39797
40680
  //#endregion
40681
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40682
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40683
+ "toLocaleString",
40684
+ "toLocaleDateString",
40685
+ "toLocaleTimeString",
40686
+ "getTimezoneOffset"
40687
+ ]);
40688
+ const containsLocaleEnvironmentRead = (expression) => {
40689
+ let readsLocaleEnvironment = false;
40690
+ walkAst(expression, (child) => {
40691
+ if (readsLocaleEnvironment) return false;
40692
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40693
+ if (child.object.name === "Intl") {
40694
+ readsLocaleEnvironment = true;
40695
+ return false;
40696
+ }
40697
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40698
+ readsLocaleEnvironment = true;
40699
+ return false;
40700
+ }
40701
+ }
40702
+ 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)) {
40703
+ readsLocaleEnvironment = true;
40704
+ return false;
40705
+ }
40706
+ });
40707
+ return readsLocaleEnvironment;
40708
+ };
40709
+ //#endregion
39798
40710
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39799
40711
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39800
40712
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39860,14 +40772,15 @@ const renderingHydrationNoFlicker = defineRule({
39860
40772
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39861
40773
  const callback = getEffectCallback(node);
39862
40774
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
39863
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39864
- if (!bodyStatements || bodyStatements.length !== 1) return;
40775
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
40776
+ if (bodyStatements.length !== 1) return;
39865
40777
  const soleStatement = bodyStatements[0];
39866
40778
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39867
40779
  const expression = soleStatement.expression;
39868
40780
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39869
40781
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39870
40782
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
40783
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39871
40784
  context.report({
39872
40785
  node,
39873
40786
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40686,6 +41599,9 @@ const rerenderFunctionalSetstate = defineRule({
40686
41599
  } })
40687
41600
  });
40688
41601
  //#endregion
41602
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41603
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41604
+ //#endregion
40689
41605
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40690
41606
  const rerenderLazyRefInit = defineRule({
40691
41607
  id: "rerender-lazy-ref-init",
@@ -40696,7 +41612,7 @@ const rerenderLazyRefInit = defineRule({
40696
41612
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40697
41613
  create: (context) => ({ CallExpression(node) {
40698
41614
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40699
- const initializer = node.arguments[0];
41615
+ const initializer = stripParenExpression(node.arguments[0]);
40700
41616
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40701
41617
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40702
41618
  if (!isPlainCall && !isNewCall) return;
@@ -40704,6 +41620,7 @@ const rerenderLazyRefInit = defineRule({
40704
41620
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40705
41621
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40706
41622
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
41623
+ if (isTrivialBuiltInConstruction(initializer)) return;
40707
41624
  if (isPlainCall && isReactHookName(calleeName)) return;
40708
41625
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40709
41626
  context.report({
@@ -40736,26 +41653,14 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40736
41653
  "getUTCMilliseconds",
40737
41654
  "valueOf"
40738
41655
  ]);
40739
- const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
40740
- "Date",
40741
- "Map",
40742
- "Set",
40743
- "WeakMap",
40744
- "WeakSet",
40745
- "RegExp",
40746
- "Error",
40747
- "URL",
40748
- "URLSearchParams",
40749
- "AbortController"
40750
- ]);
40751
41656
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40752
41657
  const findEagerInitializerCall = (expression, depth = 0) => {
40753
41658
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
40754
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40755
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40756
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40757
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40758
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41659
+ const innerExpression = stripParenExpression(expression);
41660
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41661
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41662
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41663
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40759
41664
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40760
41665
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40761
41666
  if (spreadCall) return spreadCall;
@@ -40778,7 +41683,7 @@ const rerenderLazyStateInit = defineRule({
40778
41683
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40779
41684
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40780
41685
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40781
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41686
+ if (isTrivialBuiltInConstruction(initializer)) return;
40782
41687
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40783
41688
  if (isReactHookName(calleeName)) return;
40784
41689
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41445,7 +42350,7 @@ const rnAnimationReactionAsDerived = defineRule({
41445
42350
  const body = reactionFn.body;
41446
42351
  let singleAssignment = null;
41447
42352
  if (isNodeOfType(body, "BlockStatement")) {
41448
- const statements = body.body ?? [];
42353
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41449
42354
  if (statements.length !== 1) return;
41450
42355
  const onlyStatement = statements[0];
41451
42356
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41491,7 +42396,7 @@ const rnBottomSheetPreferNative = defineRule({
41491
42396
  });
41492
42397
  //#endregion
41493
42398
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41494
- const EMPTY_VISITORS$4 = {};
42399
+ const EMPTY_VISITORS$5 = {};
41495
42400
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41496
42401
  const DETOX_ELEMENT_ACTIONS = new Set([
41497
42402
  "tap",
@@ -41519,7 +42424,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41519
42424
  "catch",
41520
42425
  "finally"
41521
42426
  ]);
41522
- const findChainRoot = (node) => {
42427
+ const findChainRoot = (wrappedNode) => {
42428
+ const node = stripParenExpression(wrappedNode);
41523
42429
  if (isNodeOfType(node, "CallExpression")) {
41524
42430
  if (isNodeOfType(node.callee, "Identifier")) return {
41525
42431
  calleeName: node.callee.name,
@@ -41551,7 +42457,7 @@ const rnDetoxMissingAwait = defineRule({
41551
42457
  recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
41552
42458
  create: (context) => {
41553
42459
  const filename = normalizeFilename(context.filename ?? "");
41554
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
42460
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41555
42461
  return { ExpressionStatement(node) {
41556
42462
  const expression = node.expression;
41557
42463
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -42107,20 +43013,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42107
43013
  };
42108
43014
  const isReactNativeDimensionsCallee = (node, callee) => {
42109
43015
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42110
- if (isNodeOfType(callee.object, "Identifier")) {
42111
- const localName = callee.object.name;
43016
+ const receiver = stripParenExpression(callee.object);
43017
+ if (isNodeOfType(receiver, "Identifier")) {
43018
+ const localName = receiver.name;
42112
43019
  const binding = findVariableInitializer(node, localName);
42113
43020
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42114
43021
  return localName === "Dimensions";
42115
43022
  }
42116
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42117
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42118
- const rootName = getRootIdentifierName(callee.object.object);
43023
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
43024
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
43025
+ const rootName = getRootIdentifierName(receiver.object);
42119
43026
  if (rootName === null) return false;
42120
43027
  const importBinding = getImportBindingForName(node, rootName);
42121
43028
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42122
43029
  }
42123
- 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";
43030
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42124
43031
  return false;
42125
43032
  };
42126
43033
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42526,7 +43433,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42526
43433
  };
42527
43434
  //#endregion
42528
43435
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42529
- const EMPTY_VISITORS$3 = {};
43436
+ const EMPTY_VISITORS$4 = {};
42530
43437
  const reportLegacyShadowProperties = (objectExpression, context) => {
42531
43438
  const legacyShadowPropertyNames = [];
42532
43439
  for (const property of objectExpression.properties ?? []) {
@@ -42549,7 +43456,7 @@ const rnNoLegacyShadowStyles = defineRule({
42549
43456
  severity: "warn",
42550
43457
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42551
43458
  create: (context) => {
42552
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
43459
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42553
43460
  return {
42554
43461
  JSXAttribute(node) {
42555
43462
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -42564,7 +43471,8 @@ const rnNoLegacyShadowStyles = defineRule({
42564
43471
  },
42565
43472
  CallExpression(node) {
42566
43473
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42567
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43474
+ const receiver = stripParenExpression(node.callee.object);
43475
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42568
43476
  if (!isMemberProperty(node.callee, "create")) return;
42569
43477
  const stylesArgument = node.arguments?.[0];
42570
43478
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43414,7 +44322,7 @@ const rnNoSetNativeProps = defineRule({
43414
44322
  const callee = node.callee;
43415
44323
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43416
44324
  if (!isNodeOfType(callee, "MemberExpression")) return;
43417
- if (!isStaticMemberNamed(callee.object, "current")) return;
44325
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43418
44326
  context.report({
43419
44327
  node,
43420
44328
  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."
@@ -43470,7 +44378,7 @@ const isExpoManagedFileActive = (context) => {
43470
44378
  };
43471
44379
  //#endregion
43472
44380
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43473
- const EMPTY_VISITORS$2 = {};
44381
+ const EMPTY_VISITORS$3 = {};
43474
44382
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43475
44383
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43476
44384
  const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
@@ -43504,7 +44412,7 @@ const rnPreferExpoImage = defineRule({
43504
44412
  severity: "warn",
43505
44413
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43506
44414
  create: (context) => {
43507
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
44415
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43508
44416
  const flaggedImports = [];
43509
44417
  const assetBindingNames = /* @__PURE__ */ new Set();
43510
44418
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -43642,14 +44550,15 @@ const analyzeGestureChain = (expression) => {
43642
44550
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43643
44551
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43644
44552
  const methodName = callee.property.name;
43645
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44553
+ const receiver = stripParenExpression(callee.object);
44554
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43646
44555
  factoryName: methodName,
43647
44556
  chainMethodNames,
43648
44557
  numberOfTapsArgument
43649
44558
  };
43650
44559
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43651
44560
  chainMethodNames.push(methodName);
43652
- cursor = callee.object;
44561
+ cursor = receiver;
43653
44562
  }
43654
44563
  return null;
43655
44564
  };
@@ -43991,7 +44900,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
43991
44900
  });
43992
44901
  //#endregion
43993
44902
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
43994
- const EMPTY_VISITORS$1 = {};
44903
+ const EMPTY_VISITORS$2 = {};
43995
44904
  const IOS_SHADOW_KEYS = new Set([
43996
44905
  "shadowColor",
43997
44906
  "shadowOffset",
@@ -44077,7 +44986,7 @@ const rnStylePreferBoxShadow = defineRule({
44077
44986
  severity: "warn",
44078
44987
  recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
44079
44988
  create: (context) => {
44080
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
44989
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44081
44990
  return {
44082
44991
  JSXAttribute(node) {
44083
44992
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44174,9 +45083,9 @@ const roleHasRequiredAriaProps = defineRule({
44174
45083
  if (!HTML_TAGS.has(elementType)) return;
44175
45084
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44176
45085
  if (!roleAttribute) return;
44177
- const roleValue = getJsxPropStringValue(roleAttribute);
44178
- if (roleValue === null) return;
44179
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
45086
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
45087
+ if (roleCandidates === null) return;
45088
+ const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44180
45089
  for (const role of roles) {
44181
45090
  const required = ROLE_REQUIRED_PROPS.get(role);
44182
45091
  if (!required) continue;
@@ -47293,7 +48202,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47293
48202
  };
47294
48203
  //#endregion
47295
48204
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47296
- const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
48205
+ const buildMessageDefault = (roles, propName) => {
48206
+ 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.`;
48207
+ };
47297
48208
  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.`;
47298
48209
  const roleSupportsAriaProps = defineRule({
47299
48210
  id: "role-supports-aria-props",
@@ -47313,6 +48224,8 @@ const roleSupportsAriaProps = defineRule({
47313
48224
  const propName = propRawName.toLowerCase();
47314
48225
  if (!propName.startsWith("aria-")) continue;
47315
48226
  if (!ARIA_PROPERTIES.has(propName)) continue;
48227
+ const attributeValue = attributeNode.value;
48228
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47316
48229
  (ariaAttributes ??= []).push({
47317
48230
  attribute,
47318
48231
  propName
@@ -47321,17 +48234,21 @@ const roleSupportsAriaProps = defineRule({
47321
48234
  if (!ariaAttributes) return;
47322
48235
  const elementType = getElementType(node, context.settings);
47323
48236
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47324
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47325
- if (!role) return;
47326
- if (!VALID_ARIA_ROLES.has(role)) return;
48237
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
48238
+ if (roleCandidates === null || roleCandidates.length === 0) return;
48239
+ const supportedSets = [];
48240
+ for (const role of roleCandidates) {
48241
+ if (!VALID_ARIA_ROLES.has(role)) return;
48242
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
48243
+ if (!supported) return;
48244
+ supportedSets.push(supported);
48245
+ }
47327
48246
  const isImplicit = !roleAttribute;
47328
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47329
- if (!supported) return;
47330
48247
  for (const { attribute, propName } of ariaAttributes) {
47331
- if (supported.has(propName)) continue;
48248
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47332
48249
  context.report({
47333
48250
  node: attribute,
47334
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
48251
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47335
48252
  });
47336
48253
  }
47337
48254
  } })
@@ -47683,11 +48600,10 @@ const isUseEffectEventSymbol = (symbol) => {
47683
48600
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47684
48601
  return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47685
48602
  };
47686
- const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47687
- const isNonReactEffectEventSymbol = (symbol, contextNode) => {
48603
+ const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47688
48604
  const initializer = symbol.initializer;
47689
48605
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47690
- return isNonReactEffectEventCallee(initializer.callee, contextNode);
48606
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47691
48607
  };
47692
48608
  const findEnclosingComponentOrHookFunction = (node) => {
47693
48609
  let current = node.parent;
@@ -47766,7 +48682,7 @@ const rulesOfHooks = defineRule({
47766
48682
  const hookContext = isHookCall$1(node, context.scopes, settings);
47767
48683
  if (!hookContext) return;
47768
48684
  const { hookName } = hookContext;
47769
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
48685
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47770
48686
  context.report({
47771
48687
  node: node.callee,
47772
48688
  message: buildEffectEventPassedDownMessage()
@@ -47879,7 +48795,7 @@ const rulesOfHooks = defineRule({
47879
48795
  Identifier(node) {
47880
48796
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47881
48797
  if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47882
- if (isNonReactEffectEventSymbol(symbol, node)) return;
48798
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
47883
48799
  if (!isSameComponentOrHookScope(symbol, node)) return;
47884
48800
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47885
48801
  context.report({
@@ -48027,7 +48943,8 @@ const serverAfterNonblocking = defineRule({
48027
48943
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48028
48944
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48029
48945
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48030
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
48946
+ const receiver = stripParenExpression(node.callee.object);
48947
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48031
48948
  if (!objectName) return;
48032
48949
  const methodName = node.callee.property.name;
48033
48950
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48703,7 +49620,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48703
49620
  };
48704
49621
  const ascendMemberChain = (referenceIdentifier) => {
48705
49622
  let chainTip = referenceIdentifier;
48706
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49623
+ while (chainTip.parent) {
49624
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49625
+ chainTip = chainTip.parent;
49626
+ continue;
49627
+ }
49628
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49629
+ chainTip = chainTip.parent;
49630
+ continue;
49631
+ }
49632
+ break;
49633
+ }
48707
49634
  return chainTip;
48708
49635
  };
48709
49636
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48728,7 +49655,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48728
49655
  const callee = callExpression.callee;
48729
49656
  if (isNodeOfType(callee, "MemberExpression")) {
48730
49657
  const methodName = getMemberPropertyName$1(callee);
48731
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49658
+ const calleeReceiver = stripParenExpression(callee.object);
49659
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48732
49660
  }
48733
49661
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48734
49662
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49458,7 +50386,7 @@ const walkServerFnChain = (outerNode) => {
49458
50386
  };
49459
50387
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49460
50388
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49461
- let currentNode = outerNode.callee.object;
50389
+ let currentNode = stripParenExpression(outerNode.callee.object);
49462
50390
  while (isNodeOfType(currentNode, "CallExpression")) {
49463
50391
  const calleeName = getCalleeName$2(currentNode);
49464
50392
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49469,7 +50397,7 @@ const walkServerFnChain = (outerNode) => {
49469
50397
  }
49470
50398
  }
49471
50399
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49472
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50400
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49473
50401
  else break;
49474
50402
  }
49475
50403
  return result;
@@ -50122,7 +51050,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50122
51050
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50123
51051
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50124
51052
  if (methodName) methodNames.unshift(methodName);
50125
- currentNode = currentNode.callee.object;
51053
+ currentNode = stripParenExpression(currentNode.callee.object);
50126
51054
  }
50127
51055
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50128
51056
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53204,6 +54132,18 @@ const reactDoctorRules = [
53204
54132
  category: "Bugs"
53205
54133
  }
53206
54134
  },
54135
+ {
54136
+ key: "react-doctor/no-locale-format-in-render",
54137
+ id: "no-locale-format-in-render",
54138
+ source: "react-doctor",
54139
+ originallyExternal: false,
54140
+ rule: {
54141
+ ...noLocaleFormatInRender,
54142
+ framework: "global",
54143
+ category: "Bugs",
54144
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54145
+ }
54146
+ },
53207
54147
  {
53208
54148
  key: "react-doctor/no-long-transition-duration",
53209
54149
  id: "no-long-transition-duration",
@@ -55451,6 +56391,34 @@ const reactDoctorRules = [
55451
56391
  ];
55452
56392
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55453
56393
  //#endregion
56394
+ //#region src/plugin/utils/is-next-file.ts
56395
+ const isNextFileActive = (context) => {
56396
+ const rawFilename = context.filename;
56397
+ if (!rawFilename) return true;
56398
+ const filename = normalizeFilename(rawFilename);
56399
+ const manifest = readNearestPackageManifest(filename);
56400
+ if (!manifest) return true;
56401
+ if (declaresDependency(manifest, "next")) return true;
56402
+ if (!declaresAnyDependency(manifest)) return true;
56403
+ const packageDirectory = findNearestPackageDirectory(filename);
56404
+ const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
56405
+ if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
56406
+ return true;
56407
+ };
56408
+ //#endregion
56409
+ //#region src/plugin/utils/wrap-nextjs-rule.ts
56410
+ const EMPTY_VISITORS$1 = {};
56411
+ const wrapNextjsRule = (rule) => {
56412
+ const innerCreate = rule.create.bind(rule);
56413
+ return {
56414
+ ...rule,
56415
+ create: (context) => {
56416
+ if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
56417
+ return innerCreate(context);
56418
+ }
56419
+ };
56420
+ };
56421
+ //#endregion
55454
56422
  //#region src/plugin/utils/wrap-react-native-rule.ts
55455
56423
  const EMPTY_VISITORS = {};
55456
56424
  const wrapReactNativeRule = (rule) => {
@@ -55920,9 +56888,14 @@ const wrapWithSemanticContext = (rule) => ({
55920
56888
  });
55921
56889
  //#endregion
55922
56890
  //#region src/plugin/react-doctor-plugin.ts
56891
+ const applyFrameworkGate = (rule) => {
56892
+ if (rule.framework === "react-native") return wrapReactNativeRule(rule);
56893
+ if (rule.framework === "nextjs") return wrapNextjsRule(rule);
56894
+ return rule;
56895
+ };
55923
56896
  const applyFrameworkRuleWrappers = (registry) => {
55924
56897
  const wrapped = {};
55925
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
56898
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55926
56899
  return wrapped;
55927
56900
  };
55928
56901
  const plugin = {