oxlint-plugin-react-doctor 0.7.2-dev.b97a92f → 0.7.2-dev.e872767

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 +1456 -520
  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)";
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2266
2322
  };
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;
2275
- };
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;
@@ -9616,7 +9945,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9616
9945
  });
9617
9946
  //#endregion
9618
9947
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9619
- const EMPTY_VISITORS$5 = {};
9948
+ const EMPTY_VISITORS$6 = {};
9620
9949
  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
9950
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9622
9951
  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 +9957,7 @@ const expoNoNonInlinedEnv = defineRule({
9628
9957
  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
9958
  create: (context) => {
9630
9959
  const filename = normalizeFilename(context.filename ?? "");
9631
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9960
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9632
9961
  return {
9633
9962
  MemberExpression(node) {
9634
9963
  if (!node.computed) return;
@@ -9881,7 +10210,10 @@ const PRAGMA = "React";
9881
10210
  const isReactFunctionCall = (node, expectedCall) => {
9882
10211
  if (!isNodeOfType(node, "CallExpression")) return false;
9883
10212
  if (getCalleeName$2(node) !== expectedCall) return false;
9884
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10213
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10214
+ const receiver = stripParenExpression(node.callee.object);
10215
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10216
+ }
9885
10217
  return true;
9886
10218
  };
9887
10219
  //#endregion
@@ -11046,8 +11378,8 @@ const interactiveSupportsFocus = defineRule({
11046
11378
  if (node.attributes.length === 0) return;
11047
11379
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11048
11380
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11049
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
- if (!role) return;
11381
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11382
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11051
11383
  let hasInteractiveHandler = false;
11052
11384
  for (const attribute of node.attributes) {
11053
11385
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11061,11 +11393,16 @@ const interactiveSupportsFocus = defineRule({
11061
11393
  const elementType = getElementType(node, context.settings);
11062
11394
  if (!HTML_TAGS.has(elementType)) return;
11063
11395
  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
11396
  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);
11397
+ const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11398
+ for (const role of roleCandidates) {
11399
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11400
+ if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11401
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11402
+ }
11403
+ const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11404
+ const roleDisplay = roleCandidates.join("' / '");
11405
+ const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11069
11406
  context.report({
11070
11407
  node,
11071
11408
  message
@@ -11254,16 +11591,6 @@ const collectHandlerReferencedNames = (root) => {
11254
11591
  return names;
11255
11592
  };
11256
11593
  //#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
11594
  //#region src/plugin/utils/get-function-binding-name.ts
11268
11595
  const getFunctionBindingIdentifier = (functionNode) => {
11269
11596
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -11963,14 +12290,15 @@ const jsCacheStorage = defineRule({
11963
12290
  "ArrowFunctionExpression:exit": exitFunctionScope,
11964
12291
  CallExpression(node) {
11965
12292
  if (!isMemberProperty(node.callee, "getItem")) return;
11966
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12293
+ const receiver = stripParenExpression(node.callee.object);
12294
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
11967
12295
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
11968
12296
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
11969
12297
  const storageKey = String(node.arguments[0].value);
11970
12298
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
11971
12299
  storageReadCounts.set(storageKey, readCount);
11972
12300
  if (readCount === 2) {
11973
- const storageName = node.callee.object.name;
12301
+ const storageName = receiver.name;
11974
12302
  context.report({
11975
12303
  node,
11976
12304
  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 +12312,16 @@ const jsCacheStorage = defineRule({
11984
12312
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
11985
12313
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
11986
12314
  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;
12315
+ if (isNodeOfType(callee, "MemberExpression")) {
12316
+ const receiver = stripParenExpression(callee.object);
12317
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12318
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12319
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12320
+ return true;
12321
+ }
12322
+ return false;
11993
12323
  }
12324
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
11994
12325
  return false;
11995
12326
  };
11996
12327
  const isChainPassThroughCall = (callExpression) => {
@@ -12002,10 +12333,7 @@ const isChainPassThroughCall = (callExpression) => {
12002
12333
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12003
12334
  let cursor = receiverNode;
12004
12335
  while (cursor) {
12005
- if (isNodeOfType(cursor, "ChainExpression")) {
12006
- cursor = cursor.expression;
12007
- continue;
12008
- }
12336
+ cursor = stripParenExpression(cursor);
12009
12337
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12010
12338
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12011
12339
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12081,10 +12409,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12081
12409
  let hops = 0;
12082
12410
  while (cursor && hops < 12) {
12083
12411
  hops += 1;
12084
- if (isNodeOfType(cursor, "ChainExpression")) {
12085
- cursor = cursor.expression;
12086
- continue;
12087
- }
12412
+ cursor = stripParenExpression(cursor);
12088
12413
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12089
12414
  const callee = cursor.callee;
12090
12415
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12108,10 +12433,7 @@ const isSmallLiteralArray = (node) => {
12108
12433
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12109
12434
  let cursor = receiverNode;
12110
12435
  while (cursor) {
12111
- if (isNodeOfType(cursor, "ChainExpression")) {
12112
- cursor = cursor.expression;
12113
- continue;
12114
- }
12436
+ cursor = stripParenExpression(cursor);
12115
12437
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12116
12438
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12117
12439
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12176,7 +12498,7 @@ const jsCombineIterations = defineRule({
12176
12498
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12177
12499
  const outerMethod = node.callee.property.name;
12178
12500
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12179
- const innerCall = node.callee.object;
12501
+ const innerCall = stripParenExpression(node.callee.object);
12180
12502
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12181
12503
  const innerMethod = innerCall.callee.property.name;
12182
12504
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12249,10 +12571,10 @@ const jsFlatmapFilter = defineRule({
12249
12571
  if (!filterArgument) return;
12250
12572
  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
12573
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12252
- const innerCall = node.callee.object;
12574
+ const innerCall = stripParenExpression(node.callee.object);
12253
12575
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12254
12576
  if (innerCall.callee.property.name !== "map") return;
12255
- const receiver = innerCall.callee.object;
12577
+ const receiver = stripParenExpression(innerCall.callee.object);
12256
12578
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12257
12579
  const elements = receiver.elements ?? [];
12258
12580
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13512,7 +13834,7 @@ const jsTosortedImmutable = defineRule({
13512
13834
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13513
13835
  create: (context) => ({ CallExpression(node) {
13514
13836
  if (!isMemberProperty(node.callee, "sort")) return;
13515
- const receiver = node.callee.object;
13837
+ const receiver = stripParenExpression(node.callee.object);
13516
13838
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13517
13839
  const spreadArgument = receiver.elements[0].argument;
13518
13840
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18549,7 +18871,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18549
18871
  };
18550
18872
  const describeClientSideNavigation = (node) => {
18551
18873
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18552
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18874
+ const receiver = stripParenExpression(node.callee.object);
18875
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18553
18876
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18554
18877
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18555
18878
  }
@@ -19169,7 +19492,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19169
19492
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19170
19493
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19171
19494
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19172
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19495
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19173
19496
  return node.callee.property.name;
19174
19497
  };
19175
19498
  const isMutatingFetchCall = (node) => {
@@ -19183,7 +19506,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19183
19506
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19184
19507
  const { property, object } = node.callee;
19185
19508
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19186
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19509
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19187
19510
  return true;
19188
19511
  };
19189
19512
  const getDbCallDescription = (node) => {
@@ -19191,7 +19514,8 @@ const getDbCallDescription = (node) => {
19191
19514
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19192
19515
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19193
19516
  const methodName = node.callee.property.name;
19194
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19517
+ const receiver = stripParenExpression(node.callee.object);
19518
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19195
19519
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19196
19520
  };
19197
19521
  const findSideEffect = (node, options = {}) => {
@@ -20591,13 +20915,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20591
20915
  if (isNodeOfType(current, "CallExpression")) {
20592
20916
  let node = ref.identifier;
20593
20917
  let parent = node.parent;
20594
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20918
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20595
20919
  node = parent;
20596
20920
  parent = node.parent;
20597
20921
  }
20598
20922
  if (current.callee === node) return current;
20599
20923
  }
20600
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20924
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20601
20925
  return null;
20602
20926
  };
20603
20927
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -20732,7 +21056,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
20732
21056
  const importDeclaration = declarationNode.parent;
20733
21057
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
20734
21058
  }));
20735
- const isHookCallee = (analysis, node, hookName) => {
21059
+ const isHookCallee$1 = (analysis, node, hookName) => {
20736
21060
  if (!node) return false;
20737
21061
  if (isNodeOfType(node, "Identifier")) {
20738
21062
  if (node.name === hookName) return true;
@@ -20741,15 +21065,19 @@ const isHookCallee = (analysis, node, hookName) => {
20741
21065
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
20742
21066
  return false;
20743
21067
  }
20744
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21068
+ if (isNodeOfType(node, "MemberExpression")) {
21069
+ const receiver = stripParenExpression(node.object);
21070
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21071
+ }
20745
21072
  return false;
20746
21073
  };
20747
21074
  const isUseEffect = (node) => {
20748
21075
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
20749
21076
  const callee = node.callee;
20750
21077
  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;
21078
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21079
+ const receiver = stripParenExpression(callee.object);
21080
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20753
21081
  };
20754
21082
  const getEffectFn = (analysis, node) => {
20755
21083
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -20777,7 +21105,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20777
21105
  const node = def.node;
20778
21106
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20779
21107
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20780
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21108
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20781
21109
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20782
21110
  const elements = node.id.elements ?? [];
20783
21111
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -20788,7 +21116,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
20788
21116
  const node = def.node;
20789
21117
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20790
21118
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20791
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21119
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20792
21120
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
20793
21121
  const elements = node.id.elements ?? [];
20794
21122
  if (elements.length !== 2) return false;
@@ -20835,7 +21163,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
20835
21163
  const node = def.node;
20836
21164
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
20837
21165
  if (!isNodeOfType(node.init, "CallExpression")) return false;
20838
- return isHookCallee(analysis, node.init.callee, "useRef");
21166
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
20839
21167
  }));
20840
21168
  const isRefCurrent = (ref) => {
20841
21169
  const parent = ref.identifier.parent;
@@ -20848,11 +21176,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
20848
21176
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
20849
21177
  const isPropCallbackInvocationRef = (analysis, ref) => {
20850
21178
  if (!isPropAlias(analysis, ref)) return false;
20851
- const identifier = ref.identifier;
20852
- const parent = identifier.parent;
21179
+ let effectiveNode = ref.identifier;
21180
+ let parent = effectiveNode.parent;
21181
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21182
+ effectiveNode = parent;
21183
+ parent = effectiveNode.parent;
21184
+ }
20853
21185
  if (!parent) return false;
20854
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20855
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21186
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21187
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20856
21188
  const memberParent = parent.parent;
20857
21189
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
20858
21190
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -20863,7 +21195,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
20863
21195
  };
20864
21196
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
20865
21197
  const getUseStateDecl = (analysis, ref) => {
20866
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21198
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20867
21199
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
20868
21200
  return node ?? null;
20869
21201
  };
@@ -21068,7 +21400,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21068
21400
  const noAdjustStateOnPropChange = defineRule({
21069
21401
  id: "no-adjust-state-on-prop-change",
21070
21402
  title: "State synced to a prop inside an effect",
21071
- severity: "error",
21403
+ severity: "warn",
21072
21404
  tags: ["test-noise"],
21073
21405
  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
21406
  create: (context) => ({ CallExpression(node) {
@@ -21109,7 +21441,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21109
21441
  "summary",
21110
21442
  "textarea"
21111
21443
  ]);
21444
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21445
+ const value = attribute.value;
21446
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21447
+ const expression = value.expression;
21448
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21449
+ };
21450
+ const DISABLEABLE_TAGS = new Set([
21451
+ "button",
21452
+ "input",
21453
+ "select",
21454
+ "textarea"
21455
+ ]);
21112
21456
  const isNativelyFocusable = (tagName, openingElement) => {
21457
+ if (DISABLEABLE_TAGS.has(tagName)) {
21458
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21459
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21460
+ }
21113
21461
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21114
21462
  switch (tagName) {
21115
21463
  case "input": {
@@ -21123,7 +21471,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21123
21471
  case "a":
21124
21472
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21125
21473
  case "audio":
21126
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21474
+ case "video": {
21475
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21476
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21477
+ }
21127
21478
  default: return false;
21128
21479
  }
21129
21480
  };
@@ -22087,7 +22438,8 @@ const SECOND_INDEX_METHODS = new Set([
22087
22438
  "some"
22088
22439
  ]);
22089
22440
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22090
- const isPositionallyStableIterationReceiver = (receiver) => {
22441
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22442
+ const receiver = stripParenExpression(receiverNode);
22091
22443
  if (isAllLiteralArrayExpression(receiver)) return true;
22092
22444
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22093
22445
  const only = receiver.elements[0];
@@ -22098,17 +22450,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22098
22450
  }
22099
22451
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22100
22452
  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;
22453
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22102
22454
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22103
22455
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22104
22456
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22105
22457
  return false;
22106
22458
  };
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
- };
22459
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22112
22460
  const isArrayFromSourcePositionallyStable = (source) => {
22113
22461
  if (isNodeOfType(source, "ObjectExpression")) {
22114
22462
  for (const property of source.properties ?? []) {
@@ -22167,18 +22515,12 @@ const expressionUsesIndex = (expression, paramName) => {
22167
22515
  return false;
22168
22516
  }
22169
22517
  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;
22518
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22171
22519
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22172
22520
  }
22173
22521
  return false;
22174
22522
  };
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
- };
22523
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22182
22524
  const noArrayIndexKey = defineRule({
22183
22525
  id: "no-array-index-key",
22184
22526
  title: "Array index used as a key",
@@ -22477,6 +22819,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22477
22819
  const section = manifest[sectionName];
22478
22820
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22479
22821
  });
22822
+ const declaresDependency = (manifest, dependencyName) => {
22823
+ for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22824
+ return false;
22825
+ };
22480
22826
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22481
22827
  const classifyPackagePlatform = (filename) => {
22482
22828
  const manifest = readNearestPackageManifest(filename);
@@ -22493,9 +22839,7 @@ const classifyPackagePlatform = (filename) => {
22493
22839
  return result;
22494
22840
  };
22495
22841
  //#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?$/;
22842
+ //#region src/plugin/utils/is-package-nested-below-project-root.ts
22499
22843
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22500
22844
  const resolveRealDirectory = (directory) => {
22501
22845
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22516,6 +22860,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22516
22860
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22517
22861
  return realPackageDirectory.startsWith(rootPrefix);
22518
22862
  };
22863
+ //#endregion
22864
+ //#region src/plugin/utils/is-react-native-file.ts
22865
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22866
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22519
22867
  const classifyReactNativeFileTarget = (context) => {
22520
22868
  const rawFilename = context.filename;
22521
22869
  if (!rawFilename) return "unknown";
@@ -22896,7 +23244,7 @@ const isAsyncFunctionLike = (node) => {
22896
23244
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
22897
23245
  return false;
22898
23246
  };
22899
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23247
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22900
23248
  "forEach",
22901
23249
  "map",
22902
23250
  "filter",
@@ -22917,31 +23265,51 @@ const runsOnEffectDispatch = (functionNode) => {
22917
23265
  if (parent.callee === functionNode) return true;
22918
23266
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
22919
23267
  const callee = parent.callee;
22920
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23268
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22921
23269
  };
22922
23270
  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) => {
23271
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23272
+ const analyzeStatementSequence = (statements, context) => {
22932
23273
  let fallThroughCount = 0;
22933
- let maxTerminatingPathCount = 0;
23274
+ let maxTerminatedCount = 0;
22934
23275
  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));
23276
+ if (isNodeOfType(statement, "IfStatement")) {
23277
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23278
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23279
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23280
+ fallThroughCount: 0,
23281
+ maxTerminatedCount: 0,
23282
+ doAllPathsTerminate: false
23283
+ };
23284
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23285
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23286
+ fallThroughCount: 0,
23287
+ maxTerminatedCount,
23288
+ doAllPathsTerminate: true
23289
+ };
23290
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23291
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
22939
23292
  continue;
22940
23293
  }
22941
- if (isTerminatingStatement(statement)) break;
23294
+ if (isTerminatingStatement(statement)) {
23295
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23296
+ return {
23297
+ fallThroughCount: 0,
23298
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23299
+ doAllPathsTerminate: true
23300
+ };
23301
+ }
22942
23302
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
22943
23303
  }
22944
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23304
+ return {
23305
+ fallThroughCount,
23306
+ maxTerminatedCount,
23307
+ doAllPathsTerminate: false
23308
+ };
23309
+ };
23310
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23311
+ const summary = analyzeStatementSequence(statements, context);
23312
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22945
23313
  };
22946
23314
  const collectLocalHelperFunctions = (root) => {
22947
23315
  const helpersByName = /* @__PURE__ */ new Map();
@@ -22968,14 +23336,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
22968
23336
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
22969
23337
  return parent.parent === effectCallback.body;
22970
23338
  };
23339
+ const countFunctionBodySetStateCalls = (functionNode, context) => {
23340
+ if (isAsyncFunctionLike(functionNode)) return 0;
23341
+ const body = functionNode.body;
23342
+ if (!body) return 0;
23343
+ return countMaxPathSetStateCalls(body, context);
23344
+ };
22971
23345
  const countMaxPathSetStateCalls = (node, context) => {
22972
23346
  if (!node || typeof node !== "object") return 0;
22973
- if (isAsyncFunctionLike(node)) return 0;
23347
+ if (isFunctionLike$1(node)) return 0;
22974
23348
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
22975
23349
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
22976
23350
  const consequent = node.consequent;
22977
23351
  const alternate = node.alternate;
22978
- return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
23352
+ const testCount = countMaxPathSetStateCalls(node.test, context);
23353
+ const thenCount = countMaxPathSetStateCalls(consequent, context);
23354
+ const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
23355
+ return testCount + Math.max(thenCount, elseCount);
22979
23356
  }
22980
23357
  if (isNodeOfType(node, "SwitchStatement")) {
22981
23358
  let maxRunSetters = 0;
@@ -23005,28 +23382,31 @@ const countMaxPathSetStateCalls = (node, context) => {
23005
23382
  }
23006
23383
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23007
23384
  let nestedSettersInArgs = 0;
23008
- for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23385
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23009
23386
  return 1 + nestedSettersInArgs;
23010
23387
  }
23011
23388
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23012
23389
  const helperFunction = context.helpersByName.get(node.callee.name);
23013
23390
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23014
23391
  context.activeHelpers.add(helperFunction);
23015
- let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23392
+ let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23016
23393
  context.activeHelpers.delete(helperFunction);
23017
23394
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23018
23395
  return helperCount;
23019
23396
  }
23020
23397
  }
23021
- const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23398
+ const countChild = (child) => {
23399
+ if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23400
+ return countMaxPathSetStateCalls(child, context);
23401
+ };
23022
23402
  let total = 0;
23023
23403
  const nodeRecord = node;
23024
23404
  for (const key of Object.keys(nodeRecord)) {
23025
23405
  if (key === "parent") continue;
23026
23406
  const child = nodeRecord[key];
23027
23407
  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);
23408
+ for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
23409
+ } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
23030
23410
  }
23031
23411
  return total;
23032
23412
  };
@@ -23060,7 +23440,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23060
23440
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23061
23441
  const firstStatement = (body.body ?? [])[0];
23062
23442
  if (!firstStatement) return false;
23063
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23443
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23444
+ const consequent = firstStatement.consequent;
23445
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23064
23446
  return mentionsDevEnvFlag(firstStatement.test);
23065
23447
  };
23066
23448
  const noCascadingSetState = defineRule({
@@ -23075,7 +23457,7 @@ const noCascadingSetState = defineRule({
23075
23457
  const callback = getEffectCallback(node);
23076
23458
  if (!callback) return;
23077
23459
  if (isDevOnlyGuardedEffect(callback)) return;
23078
- const setStateCallCount = countMaxPathSetStateCalls(callback, {
23460
+ const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23079
23461
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23080
23462
  activeHelpers: /* @__PURE__ */ new Set(),
23081
23463
  effectCallback: callback
@@ -23419,40 +23801,6 @@ const noCloneElement = defineRule({
23419
23801
  } })
23420
23802
  });
23421
23803
  //#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
23804
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23457
23805
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23458
23806
  const CONTEXT_MODULES = [
@@ -24423,11 +24771,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24423
24771
  return isInitialOnlyPropName(arg.name);
24424
24772
  };
24425
24773
  //#endregion
24774
+ //#region src/plugin/utils/is-no-op-statement.ts
24775
+ const isNoOpStatement = (statement) => {
24776
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24777
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24778
+ const expression = stripParenExpression(statement.expression);
24779
+ if (isNodeOfType(expression, "Literal")) return true;
24780
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24781
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24782
+ return false;
24783
+ };
24784
+ //#endregion
24426
24785
  //#region src/plugin/utils/get-callback-statements.ts
24427
24786
  const getCallbackStatements = (callback) => {
24428
24787
  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] : [];
24788
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24431
24789
  };
24432
24790
  //#endregion
24433
24791
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24466,6 +24824,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24466
24824
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24467
24825
  }
24468
24826
  };
24827
+ const flattenGuardedStatements = (statements) => {
24828
+ const flattened = [];
24829
+ for (const statement of statements) {
24830
+ if (isNoOpStatement(statement)) continue;
24831
+ if (isNodeOfType(statement, "ExpressionStatement")) {
24832
+ flattened.push(statement);
24833
+ continue;
24834
+ }
24835
+ if (isNodeOfType(statement, "IfStatement")) {
24836
+ for (const branch of [statement.consequent, statement.alternate]) {
24837
+ if (!branch) continue;
24838
+ const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24839
+ if (flattenedBranch === null) return null;
24840
+ flattened.push(...flattenedBranch);
24841
+ }
24842
+ continue;
24843
+ }
24844
+ return null;
24845
+ }
24846
+ return flattened;
24847
+ };
24469
24848
  const noDerivedStateEffect = defineRule({
24470
24849
  id: "no-derived-state-effect",
24471
24850
  title: "Derived state stored in an effect",
@@ -24497,8 +24876,8 @@ const noDerivedStateEffect = defineRule({
24497
24876
  }
24498
24877
  }
24499
24878
  if (sawAnyDep && allDepsAreInitialOnly) return;
24500
- const statements = getCallbackStatements(callback);
24501
- if (statements.length === 0) return;
24879
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24880
+ if (statements === null || statements.length === 0) return;
24502
24881
  if (!statements.every((statement) => {
24503
24882
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24504
24883
  const expression = statement.expression;
@@ -25133,7 +25512,7 @@ const noDidMountSetState = defineRule({
25133
25512
  const { mode } = resolveSettings$20(context.settings);
25134
25513
  return { CallExpression(node) {
25135
25514
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25136
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25515
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25137
25516
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25138
25517
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25139
25518
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25258,7 +25637,7 @@ const noDidUpdateSetState = defineRule({
25258
25637
  const { mode } = resolveSettings$19(context.settings);
25259
25638
  return { CallExpression(node) {
25260
25639
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25261
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25640
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25262
25641
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25263
25642
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25264
25643
  if (isInsideDiffGuard(node)) return;
@@ -25579,9 +25958,10 @@ const noDocumentStartViewTransition = defineRule({
25579
25958
  create: (context) => ({ CallExpression(node) {
25580
25959
  const callee = node.callee;
25581
25960
  if (!isNodeOfType(callee, "MemberExpression")) return;
25582
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
25961
+ const receiver = stripParenExpression(callee.object);
25962
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25583
25963
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25584
- if (context.scopes.symbolFor(callee.object) !== null) return;
25964
+ if (context.scopes.symbolFor(receiver) !== null) return;
25585
25965
  if (!importsReactViewTransition(node)) return;
25586
25966
  context.report({
25587
25967
  node,
@@ -25601,7 +25981,8 @@ const noDocumentWrite = defineRule({
25601
25981
  create: (context) => ({ CallExpression(node) {
25602
25982
  const callee = node.callee;
25603
25983
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25604
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
25984
+ const receiver = stripParenExpression(callee.object);
25985
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25605
25986
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25606
25987
  context.report({
25607
25988
  node,
@@ -26293,7 +26674,12 @@ const noEffectEventInDeps = defineRule({
26293
26674
  const initializer = declaratorNode.init;
26294
26675
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26295
26676
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26296
- if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26677
+ if (isNodeOfType(initializer.callee, "Identifier")) {
26678
+ if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26679
+ const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26680
+ if (calleeSymbol && calleeSymbol.kind !== "import") return;
26681
+ }
26682
+ if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
26297
26683
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26298
26684
  } });
26299
26685
  return {
@@ -28873,7 +29259,7 @@ const noIsMounted = defineRule({
28873
29259
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
28874
29260
  create: (context) => ({ CallExpression(node) {
28875
29261
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
28876
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29262
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28877
29263
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
28878
29264
  if (!getParentComponent(node)) return;
28879
29265
  context.report({
@@ -28888,7 +29274,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
28888
29274
  const isJsonMethodCall = (node, method) => {
28889
29275
  if (!isNodeOfType(node, "CallExpression")) return false;
28890
29276
  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;
29277
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29278
+ const receiver = stripParenExpression(callee.object);
29279
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28892
29280
  };
28893
29281
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
28894
29282
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -28973,7 +29361,7 @@ const extractReturnTypeAnnotation = (returnType) => {
28973
29361
  const noJsxElementType = defineRule({
28974
29362
  id: "no-jsx-element-type",
28975
29363
  title: "No JSX.Element",
28976
- severity: "error",
29364
+ severity: "warn",
28977
29365
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
28978
29366
  create: (context) => {
28979
29367
  let isJsxImported = false;
@@ -29299,6 +29687,378 @@ const noLegacyContextApi = defineRule({
29299
29687
  }
29300
29688
  });
29301
29689
  //#endregion
29690
+ //#region src/plugin/utils/executes-during-render.ts
29691
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29692
+ "map",
29693
+ "filter",
29694
+ "forEach",
29695
+ "flatMap",
29696
+ "reduce",
29697
+ "reduceRight",
29698
+ "some",
29699
+ "every",
29700
+ "find",
29701
+ "findIndex",
29702
+ "findLast",
29703
+ "findLastIndex",
29704
+ "sort",
29705
+ "toSorted"
29706
+ ]);
29707
+ const executesDuringRender = (functionNode) => {
29708
+ const parent = functionNode.parent;
29709
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29710
+ if (parent.callee === functionNode) return true;
29711
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29712
+ 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;
29713
+ };
29714
+ //#endregion
29715
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29716
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29717
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29718
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29719
+ return false;
29720
+ };
29721
+ //#endregion
29722
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29723
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29724
+ let ancestor = bindingIdentifier.parent;
29725
+ while (ancestor) {
29726
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29727
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29728
+ ancestor = ancestor.parent ?? null;
29729
+ }
29730
+ return null;
29731
+ };
29732
+ //#endregion
29733
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29734
+ const isFalsyLiteral = (node) => {
29735
+ if (!node) return true;
29736
+ if (isNodeOfType(node, "Literal")) return !node.value;
29737
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29738
+ };
29739
+ const isFalsyInitialStateBinding = (identifier) => {
29740
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29741
+ const binding = findVariableInitializer(identifier, identifier.name);
29742
+ if (!binding) return false;
29743
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29744
+ if (!declarator?.init) return false;
29745
+ const init = stripParenExpression(declarator.init);
29746
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29747
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29748
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29749
+ return isFalsyLiteral(init.arguments?.[0]);
29750
+ };
29751
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29752
+ //#endregion
29753
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29754
+ const isGatedByFalsyInitialState = (node) => {
29755
+ let cursor = node;
29756
+ let parent = node.parent;
29757
+ while (parent) {
29758
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29759
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29760
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29761
+ cursor = parent;
29762
+ parent = parent.parent ?? null;
29763
+ }
29764
+ return false;
29765
+ };
29766
+ //#endregion
29767
+ //#region src/plugin/utils/references-client-only-flag.ts
29768
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29769
+ const referencesClientOnlyFlag = (expression) => {
29770
+ const unwrapped = stripParenExpression(expression);
29771
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29772
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29773
+ const property = unwrapped.property;
29774
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29775
+ }
29776
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29777
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29778
+ return false;
29779
+ };
29780
+ //#endregion
29781
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29782
+ const isInsideClientOnlyGuard = (node) => {
29783
+ let cursor = node;
29784
+ let parent = node.parent;
29785
+ while (parent) {
29786
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29787
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29788
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29789
+ cursor = parent;
29790
+ parent = parent.parent ?? null;
29791
+ }
29792
+ return false;
29793
+ };
29794
+ //#endregion
29795
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29796
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29797
+ "toLocaleString",
29798
+ "toLocaleDateString",
29799
+ "toLocaleTimeString"
29800
+ ]);
29801
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29802
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29803
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29804
+ "format",
29805
+ "formatToParts",
29806
+ "formatRange"
29807
+ ]);
29808
+ const isProvableDateExpression = (expression) => {
29809
+ if (!expression) return false;
29810
+ const unwrapped = stripParenExpression(expression);
29811
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29812
+ };
29813
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29814
+ const receiverNameLooksDateFlavored = (expression) => {
29815
+ if (!expression) return false;
29816
+ const unwrapped = stripParenExpression(expression);
29817
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29818
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29819
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29820
+ return false;
29821
+ };
29822
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29823
+ if (!objectExpression) return false;
29824
+ const unwrapped = stripParenExpression(objectExpression);
29825
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29826
+ for (const property of unwrapped.properties ?? []) {
29827
+ if (!isNodeOfType(property, "Property")) continue;
29828
+ if (property.computed) continue;
29829
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29830
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29831
+ }
29832
+ return false;
29833
+ };
29834
+ const hasExplicitLocaleArgument = (argument) => {
29835
+ if (!argument) return false;
29836
+ const unwrapped = stripParenExpression(argument);
29837
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29838
+ return true;
29839
+ };
29840
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29841
+ const localeArgument = call.arguments?.[0];
29842
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29843
+ const optionsArgument = call.arguments?.[1];
29844
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29845
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29846
+ };
29847
+ const matchLocaleMethodCall = (call) => {
29848
+ const callee = call.callee;
29849
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29850
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29851
+ const methodName = callee.property.name;
29852
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29853
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29854
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29855
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29856
+ return {
29857
+ node: call,
29858
+ display: `${methodName}()`
29859
+ };
29860
+ };
29861
+ const getIntlFormatterName = (expression) => {
29862
+ if (!expression) return null;
29863
+ const unwrapped = stripParenExpression(expression);
29864
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29865
+ const callee = unwrapped.callee;
29866
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29867
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29868
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29869
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29870
+ };
29871
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29872
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29873
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29874
+ if (formatterName !== "DateTimeFormat") return true;
29875
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29876
+ };
29877
+ const matchIntlFormatCall = (call) => {
29878
+ const callee = call.callee;
29879
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29880
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29881
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29882
+ let construction = stripParenExpression(callee.object);
29883
+ if (isNodeOfType(construction, "Identifier")) {
29884
+ const binding = findVariableInitializer(construction, construction.name);
29885
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29886
+ }
29887
+ if (!construction) return null;
29888
+ const formatterName = getIntlFormatterName(construction);
29889
+ if (!formatterName) return null;
29890
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29891
+ return {
29892
+ node: call,
29893
+ display: `Intl.${formatterName}().${callee.property.name}()`
29894
+ };
29895
+ };
29896
+ const isDeterministicInputDateConstruction = (expression) => {
29897
+ if (!expression) return false;
29898
+ const unwrapped = stripParenExpression(expression);
29899
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29900
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29901
+ return (unwrapped.arguments?.length ?? 0) > 0;
29902
+ };
29903
+ const matchDateDefaultStringification = (node) => {
29904
+ if (isNodeOfType(node, "CallExpression")) {
29905
+ const callee = node.callee;
29906
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29907
+ node,
29908
+ display: "Date.prototype.toString()"
29909
+ };
29910
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29911
+ node,
29912
+ display: "String(new Date(…))"
29913
+ };
29914
+ return null;
29915
+ }
29916
+ if (isNodeOfType(node, "TemplateLiteral")) {
29917
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29918
+ node: expression,
29919
+ display: "`${new Date(…)}`"
29920
+ };
29921
+ }
29922
+ return null;
29923
+ };
29924
+ const findRenderPhaseComponentOrHook = (node) => {
29925
+ let functionNode = findEnclosingFunction(node);
29926
+ while (functionNode) {
29927
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29928
+ if (!executesDuringRender(functionNode)) return null;
29929
+ functionNode = findEnclosingFunction(functionNode);
29930
+ }
29931
+ return null;
29932
+ };
29933
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29934
+ if (fileHasUseClientDirective) return true;
29935
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
29936
+ if (displayName && isReactHookName(displayName)) return true;
29937
+ let callsHook = false;
29938
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
29939
+ if (callsHook) return false;
29940
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
29941
+ callsHook = true;
29942
+ return false;
29943
+ }
29944
+ });
29945
+ return callsHook;
29946
+ };
29947
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
29948
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
29949
+ if (!isNodeOfType(body, "BlockStatement")) return false;
29950
+ const ancestors = /* @__PURE__ */ new Set();
29951
+ let cursor = node;
29952
+ while (cursor) {
29953
+ ancestors.add(cursor);
29954
+ cursor = cursor.parent ?? null;
29955
+ }
29956
+ for (const statement of body.body ?? []) {
29957
+ if (ancestors.has(statement)) return false;
29958
+ if (!isNodeOfType(statement, "IfStatement")) continue;
29959
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
29960
+ let returnsEarly = false;
29961
+ walkAst(statement.consequent, (child) => {
29962
+ if (isFunctionLike$1(child)) return false;
29963
+ if (isNodeOfType(child, "ReturnStatement")) {
29964
+ returnsEarly = true;
29965
+ return false;
29966
+ }
29967
+ });
29968
+ if (returnsEarly) return true;
29969
+ }
29970
+ return false;
29971
+ };
29972
+ const findEnclosingJsxOpeningElement = (node) => {
29973
+ let cursor = node.parent;
29974
+ while (cursor) {
29975
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
29976
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
29977
+ cursor = cursor.parent ?? null;
29978
+ }
29979
+ return null;
29980
+ };
29981
+ const noLocaleFormatInRender = defineRule({
29982
+ id: "no-locale-format-in-render",
29983
+ title: "Locale/timezone formatting during render",
29984
+ severity: "warn",
29985
+ category: "Correctness",
29986
+ disabledWhen: [
29987
+ "vite",
29988
+ "cra",
29989
+ "expo",
29990
+ "react-native",
29991
+ "unknown"
29992
+ ],
29993
+ 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.",
29994
+ create: (context) => {
29995
+ if (isTestlikeFilename(context.filename)) return {};
29996
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
29997
+ let fileHasUseClientDirective = false;
29998
+ let fileIsEmailTemplate = false;
29999
+ const reportedNodes = /* @__PURE__ */ new Set();
30000
+ const reportIfRenderPhase = (match) => {
30001
+ if (reportedNodes.has(match.node)) return;
30002
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30003
+ if (!componentOrHookNode) return;
30004
+ if (fileIsEmailTemplate) return;
30005
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30006
+ if (isInsideClientOnlyGuard(match.node)) return;
30007
+ if (isGatedByFalsyInitialState(match.node)) return;
30008
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30009
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30010
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30011
+ reportedNodes.add(match.node);
30012
+ context.report({
30013
+ node: match.node,
30014
+ 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.`
30015
+ });
30016
+ };
30017
+ return {
30018
+ Program(node) {
30019
+ fileHasUseClientDirective = hasDirective(node, "use client");
30020
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30021
+ },
30022
+ CallExpression(node) {
30023
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30024
+ if (match) reportIfRenderPhase(match);
30025
+ },
30026
+ TemplateLiteral(node) {
30027
+ const match = matchDateDefaultStringification(node);
30028
+ if (match) reportIfRenderPhase(match);
30029
+ },
30030
+ JSXExpressionContainer(node) {
30031
+ const expression = stripParenExpression(node.expression);
30032
+ if (!isNodeOfType(expression, "CallExpression")) return;
30033
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30034
+ const helperName = expression.callee.name;
30035
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30036
+ if (!componentOrHookNode) return;
30037
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30038
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30039
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30040
+ walkAst(helperNode.body ?? helperNode, (child) => {
30041
+ if (isFunctionLike$1(child)) return false;
30042
+ if (!isNodeOfType(child, "CallExpression")) return;
30043
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30044
+ if (!match || reportedNodes.has(match.node)) return;
30045
+ if (fileIsEmailTemplate) return;
30046
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30047
+ if (isInsideClientOnlyGuard(node)) return;
30048
+ if (isGatedByFalsyInitialState(node)) return;
30049
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30050
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30051
+ reportedNodes.add(match.node);
30052
+ context.report({
30053
+ node: match.node,
30054
+ 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.`
30055
+ });
30056
+ });
30057
+ }
30058
+ };
30059
+ }
30060
+ });
30061
+ //#endregion
29302
30062
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29303
30063
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29304
30064
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30112,7 +30872,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30112
30872
  "reverse",
30113
30873
  "sort"
30114
30874
  ]);
30115
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30116
30875
  const OBJECT_MUTATION_METHODS = new Set([
30117
30876
  "assign",
30118
30877
  "defineProperties",
@@ -30186,7 +30945,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30186
30945
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30187
30946
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30188
30947
  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
30948
  }
30191
30949
  }
30192
30950
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30225,6 +30983,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30225
30983
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30226
30984
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30227
30985
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
30986
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30228
30987
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30229
30988
  });
30230
30989
  return mutations;
@@ -30457,7 +31216,7 @@ const noNestedComponentDefinition = defineRule({
30457
31216
  id: "no-nested-component-definition",
30458
31217
  title: "Component defined inside another component",
30459
31218
  tags: ["test-noise", "react-jsx-only"],
30460
- severity: "error",
31219
+ severity: "warn",
30461
31220
  category: "Correctness",
30462
31221
  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
31222
  create: (context) => {
@@ -31412,12 +32171,12 @@ const noPassDataToParent = defineRule({
31412
32171
  if (calleeNode === identifier) {
31413
32172
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31414
32173
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31415
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32174
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31416
32175
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31417
32176
  if (isCustomHookParameter(ref)) continue;
31418
32177
  } else continue;
31419
32178
  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));
32179
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31421
32180
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31422
32181
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31423
32182
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31608,7 +32367,7 @@ const noPassLiveStateToParent = defineRule({
31608
32367
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31609
32368
  const calleeNode = callExpr.callee;
31610
32369
  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));
32370
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31612
32371
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31613
32372
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31614
32373
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -31936,40 +32695,6 @@ const noPreventDefault = defineRule({
31936
32695
  }
31937
32696
  });
31938
32697
  //#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
32698
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
31974
32699
  const isRefLatchGuardedEffect = (callbackBody) => {
31975
32700
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32176,7 +32901,7 @@ const isAlwaysFreshExpression = (expression) => {
32176
32901
  return `${callee.name}()`;
32177
32902
  }
32178
32903
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32179
- const receiver = callee.object;
32904
+ const receiver = stripParenExpression(callee.object);
32180
32905
  const property = callee.property;
32181
32906
  if (!isNodeOfType(property, "Identifier")) return null;
32182
32907
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32297,8 +33022,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32297
33022
  if (typeof sourceValue !== "string") return;
32298
33023
  if (handleExtraSource?.(node, context)) return;
32299
33024
  if (sourceValue !== source) return;
33025
+ if (isTypeOnlyImport(node)) return;
32300
33026
  for (const specifier of node.specifiers ?? []) {
32301
33027
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33028
+ if (specifier.importKind === "type") continue;
32302
33029
  const importedName = getImportedName$1(specifier);
32303
33030
  if (!importedName) continue;
32304
33031
  const message = messages.get(importedName);
@@ -32317,8 +33044,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32317
33044
  MemberExpression(node) {
32318
33045
  if (namespaceBindings.size === 0) return;
32319
33046
  if (node.computed) return;
32320
- if (!isNodeOfType(node.object, "Identifier")) return;
32321
- if (!namespaceBindings.has(node.object.name)) return;
33047
+ const receiver = stripParenExpression(node.object);
33048
+ if (!isNodeOfType(receiver, "Identifier")) return;
33049
+ if (!namespaceBindings.has(receiver.name)) return;
32322
33050
  if (!isNodeOfType(node.property, "Identifier")) return;
32323
33051
  const message = messages.get(node.property.name);
32324
33052
  if (message) context.report({
@@ -32383,7 +33111,7 @@ const noReactDomDeprecatedApis = defineRule({
32383
33111
  });
32384
33112
  //#endregion
32385
33113
  //#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'`."]]);
33114
+ 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
33115
  const isVendoredShadcnUiFilename = (rawFilename) => {
32388
33116
  if (!rawFilename) return false;
32389
33117
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32421,7 +33149,7 @@ const noReact19DeprecatedApis = defineRule({
32421
33149
  requires: ["react:19"],
32422
33150
  tags: ["test-noise", "migration-hint"],
32423
33151
  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.",
33152
+ 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
33153
  create: (context) => {
32426
33154
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32427
33155
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -32745,34 +33473,11 @@ const noRedundantShouldComponentUpdate = defineRule({
32745
33473
  });
32746
33474
  //#endregion
32747
33475
  //#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
33476
  const isInsideComponentContext = (node) => {
32772
33477
  let cursor = node.parent;
32773
33478
  while (cursor) {
32774
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
32775
33479
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33480
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
32776
33481
  cursor = cursor.parent ?? null;
32777
33482
  }
32778
33483
  return false;
@@ -32782,24 +33487,28 @@ const functionBodyOf = (node) => {
32782
33487
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
32783
33488
  return null;
32784
33489
  };
33490
+ const isHookCallee = (callee) => {
33491
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33492
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33493
+ return false;
33494
+ };
32785
33495
  const containsHookCall = (body) => {
32786
33496
  let found = false;
32787
33497
  walkAst(body, (child) => {
32788
- if (found) return;
33498
+ if (found) return false;
33499
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32789
33500
  if (!isNodeOfType(child, "CallExpression")) return;
32790
- const name = getCalleeName$2(child);
32791
- if (name && isReactHookName(name)) found = true;
33501
+ if (isHookCallee(child.callee)) found = true;
32792
33502
  });
32793
33503
  return found;
32794
33504
  };
32795
- const isModuleScopeHookFreeHelper = (symbol) => {
33505
+ const isHookCallingRenderHelper = (symbol) => {
32796
33506
  if (!symbol) return false;
32797
33507
  const declaration = symbol.declarationNode;
32798
33508
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
32799
33509
  const body = functionBodyOf(declaration);
32800
33510
  if (!body) return false;
32801
- if (findEnclosingFunction(declaration) !== null) return false;
32802
- return !containsHookCall(body);
33511
+ return containsHookCall(body);
32803
33512
  };
32804
33513
  const noRenderInRender = defineRule({
32805
33514
  id: "no-render-in-render",
@@ -32808,20 +33517,13 @@ const noRenderInRender = defineRule({
32808
33517
  tags: ["test-noise"],
32809
33518
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
32810
33519
  create: (context) => ({ JSXExpressionContainer(node) {
32811
- const expression = node.expression;
33520
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32812
33521
  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;
33522
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33523
+ const calleeName = expression.callee.name;
33524
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32817
33525
  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
- }
33526
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32825
33527
  context.report({
32826
33528
  node: expression,
32827
33529
  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 +33584,7 @@ const noRenderPropChildren = defineRule({
32882
33584
  //#endregion
32883
33585
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
32884
33586
  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
- };
33587
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32892
33588
  const isUsedAsReturnValue = (parent) => {
32893
33589
  if (!parent) return false;
32894
33590
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33724,7 +34420,7 @@ const noSetState = defineRule({
33724
34420
  category: "Architecture",
33725
34421
  create: (context) => ({ CallExpression(node) {
33726
34422
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33727
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34423
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33728
34424
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33729
34425
  if (!getParentComponent(node)) return;
33730
34426
  context.report({
@@ -36170,6 +36866,36 @@ const isTriviallyCheapExpression = (node) => {
36170
36866
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36171
36867
  return true;
36172
36868
  };
36869
+ const isTrivialContainerLiteral = (node) => {
36870
+ if (!node) return false;
36871
+ const innerExpression = stripParenExpression(node);
36872
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36873
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36874
+ return false;
36875
+ };
36876
+ const isNonEscapingRead = (identifier) => {
36877
+ const readRoot = findTransparentExpressionRoot(identifier);
36878
+ const memberNode = readRoot.parent;
36879
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
36880
+ const memberUse = findTransparentExpressionRoot(memberNode);
36881
+ const memberUseParent = memberUse.parent;
36882
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
36883
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
36884
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
36885
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
36886
+ };
36887
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
36888
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
36889
+ const parentNode = memoUsageRoot.parent;
36890
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36891
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
36892
+ const bindingTarget = parentNode.id;
36893
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36894
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36895
+ const symbol = scopes.symbolFor(bindingTarget);
36896
+ if (!symbol) return false;
36897
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36898
+ };
36173
36899
  const noUsememoSimpleExpression = defineRule({
36174
36900
  id: "no-usememo-simple-expression",
36175
36901
  title: "useMemo on a cheap value",
@@ -36191,9 +36917,17 @@ const noUsememoSimpleExpression = defineRule({
36191
36917
  let returnExpression = null;
36192
36918
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36193
36919
  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({
36920
+ if (!returnExpression) return;
36921
+ if (isTriviallyCheapExpression(returnExpression)) {
36922
+ context.report({
36923
+ node,
36924
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36925
+ });
36926
+ return;
36927
+ }
36928
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36195
36929
  node,
36196
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36930
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36197
36931
  });
36198
36932
  } })
36199
36933
  });
@@ -36299,7 +37033,7 @@ const noWillUpdateSetState = defineRule({
36299
37033
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36300
37034
  return { CallExpression(node) {
36301
37035
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36302
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37036
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36303
37037
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36304
37038
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36305
37039
  context.report({
@@ -36575,8 +37309,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36575
37309
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36576
37310
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36577
37311
  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.";
37312
+ 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
37313
  const DEFAULT_REACT_HOCS = [
36581
37314
  "memo",
36582
37315
  "forwardRef",
@@ -36647,6 +37380,20 @@ const isReactComponentInitializer = (expression, state) => {
36647
37380
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36648
37381
  return false;
36649
37382
  };
37383
+ const objectExpressionBundlesComponents = (objectExpression, state) => {
37384
+ for (const property of objectExpression.properties ?? []) {
37385
+ if (!isNodeOfType(property, "Property")) continue;
37386
+ const value = skipTsExpression(property.value);
37387
+ if (isNodeOfType(value, "Identifier")) {
37388
+ if (state.localComponentNames.has(value.name)) return true;
37389
+ continue;
37390
+ }
37391
+ if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
37392
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
37393
+ if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
37394
+ }
37395
+ return false;
37396
+ };
36650
37397
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36651
37398
  if (initializer) {
36652
37399
  const expression = skipTsExpression(initializer);
@@ -36677,6 +37424,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36677
37424
  reportNode
36678
37425
  };
36679
37426
  }
37427
+ if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
37428
+ kind: "namespace-object",
37429
+ reportNode
37430
+ };
36680
37431
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36681
37432
  kind: "non-component",
36682
37433
  reportNode
@@ -36693,7 +37444,7 @@ const collectRelevantNodes = (programRoot) => {
36693
37444
  walkAst(programRoot, (child) => {
36694
37445
  const childType = child.type;
36695
37446
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36696
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
37447
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36697
37448
  });
36698
37449
  return {
36699
37450
  exportNodes,
@@ -36738,18 +37489,35 @@ const onlyExportComponents = defineRule({
36738
37489
  category: "Architecture",
36739
37490
  create: (context) => {
36740
37491
  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
37492
  return { Program(node) {
36747
37493
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
36748
37494
  const { exportNodes, componentCandidates } = collectRelevantNodes(node);
37495
+ const localComponentNames = /* @__PURE__ */ new Set();
37496
+ const state = {
37497
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
37498
+ allowExportNames: new Set(settings.allowExportNames),
37499
+ allowConstantExport: settings.allowConstantExport,
37500
+ localComponentNames,
37501
+ scopes: context.scopes
37502
+ };
37503
+ for (const child of componentCandidates) {
37504
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
37505
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
37506
+ }
37507
+ if (isNodeOfType(child, "ClassDeclaration") && child.id) {
37508
+ if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37509
+ }
37510
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
37511
+ const initializer = child.init;
37512
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37513
+ const expression = initializer ? skipTsExpression(initializer) : null;
37514
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37515
+ }
37516
+ }
37517
+ }
36749
37518
  const exports = [];
36750
37519
  let hasReactExport = false;
36751
37520
  let hasAnyExports = false;
36752
- const localComponents = [];
36753
37521
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
36754
37522
  for (const child of exportNodes) {
36755
37523
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -36820,7 +37588,14 @@ const onlyExportComponents = defineRule({
36820
37588
  });
36821
37589
  continue;
36822
37590
  }
36823
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
37591
+ if (isNodeOfType(stripped, "ObjectExpression")) {
37592
+ context.report({
37593
+ node: stripped,
37594
+ message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
37595
+ });
37596
+ continue;
37597
+ }
37598
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
36824
37599
  context.report({
36825
37600
  node: stripped,
36826
37601
  message: ANONYMOUS_MESSAGE
@@ -36873,32 +37648,23 @@ const onlyExportComponents = defineRule({
36873
37648
  let entry;
36874
37649
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
36875
37650
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
36876
- else entry = {
36877
- kind: "non-component",
36878
- reportNode
36879
- };
37651
+ else {
37652
+ entry = {
37653
+ kind: "non-component",
37654
+ reportNode
37655
+ };
37656
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
37657
+ }
36880
37658
  if (isReExportFromSource && entry.kind !== "react-component") continue;
36881
37659
  exports.push(entry);
36882
37660
  }
36883
37661
  }
36884
37662
  }
36885
37663
  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
- }
37664
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
37665
+ node: entry.reportNode,
37666
+ message: NAMESPACE_OBJECT_MESSAGE
37667
+ });
36902
37668
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
36903
37669
  if (entry.kind === "non-component") context.report({
36904
37670
  node: entry.reportNode,
@@ -36909,14 +37675,6 @@ const onlyExportComponents = defineRule({
36909
37675
  message: REACT_CONTEXT_MESSAGE
36910
37676
  });
36911
37677
  }
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
37678
  } };
36921
37679
  }
36922
37680
  });
@@ -37714,8 +38472,8 @@ const preferHtmlDialog = defineRule({
37714
38472
  if (!HTML_TAGS.has(tagName)) return;
37715
38473
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37716
38474
  if (roleAttribute) {
37717
- const roleValue = getJsxPropStringValue(roleAttribute);
37718
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
38475
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
38476
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37719
38477
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37720
38478
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37721
38479
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -38419,11 +39177,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38419
39177
  }
38420
39178
  return null;
38421
39179
  };
39180
+ const isTrivialLiteralExpression = (expression) => {
39181
+ if (isNodeOfType(expression, "Literal")) return true;
39182
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39183
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39184
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39185
+ return false;
39186
+ };
38422
39187
  const getSingleSetterCallFromHandler = (handler) => {
38423
39188
  const handlerStatements = getCallbackStatements(handler);
38424
39189
  if (handlerStatements.length !== 1) return null;
38425
39190
  const onlyStatement = handlerStatements[0];
38426
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39191
+ let expression = onlyStatement;
39192
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39193
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39194
+ if (!expression) return null;
39195
+ expression = stripParenExpression(expression);
38427
39196
  if (!isNodeOfType(expression, "CallExpression")) return null;
38428
39197
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38429
39198
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38433,6 +39202,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38433
39202
  setterArgument: expression.arguments[0]
38434
39203
  };
38435
39204
  };
39205
+ const isListenerCollectionInitializer = (init) => {
39206
+ if (!init) return false;
39207
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39208
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39209
+ };
39210
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39211
+ if (!isFunctionLike$1(functionNode)) return false;
39212
+ const firstParam = functionNode.params?.[0];
39213
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39214
+ const listenerParamName = firstParam.name;
39215
+ let registersListener = false;
39216
+ walkAst(functionNode.body, (child) => {
39217
+ if (registersListener) return false;
39218
+ if (!isNodeOfType(child, "CallExpression")) return;
39219
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39220
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39221
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39222
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39223
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39224
+ const registeredArgument = child.arguments?.[0];
39225
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39226
+ });
39227
+ return registersListener;
39228
+ };
39229
+ const buildModuleScopeStoreIndex = (programRoot) => {
39230
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39231
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39232
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39233
+ if (!isNodeOfType(programRoot, "Program")) return {
39234
+ mutableBindingNames,
39235
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39236
+ };
39237
+ for (const statement of programRoot.body ?? []) {
39238
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39239
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39240
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39241
+ continue;
39242
+ }
39243
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39244
+ for (const declarator of unwrapped.declarations ?? []) {
39245
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39246
+ const init = declarator.init ?? null;
39247
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39248
+ mutableBindingNames.add(declarator.id.name);
39249
+ continue;
39250
+ }
39251
+ if (isListenerCollectionInitializer(init)) {
39252
+ listenerCollectionNames.add(declarator.id.name);
39253
+ continue;
39254
+ }
39255
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39256
+ }
39257
+ }
39258
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39259
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39260
+ return {
39261
+ mutableBindingNames,
39262
+ subscribeFunctionNames
39263
+ };
39264
+ };
39265
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39266
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39267
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39268
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39269
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39270
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39271
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39272
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39273
+ return initialArgument.name;
39274
+ };
39275
+ const argumentForwardsSetter = (argument, setterName) => {
39276
+ if (!argument) return false;
39277
+ const unwrapped = stripParenExpression(argument);
39278
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39279
+ if (!isFunctionLike$1(unwrapped)) return false;
39280
+ let callsSetter = false;
39281
+ walkAst(unwrapped.body, (child) => {
39282
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39283
+ callsSetter = true;
39284
+ return false;
39285
+ }
39286
+ });
39287
+ return callsSetter;
39288
+ };
39289
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39290
+ let matchedCall = null;
39291
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39292
+ if (matchedCall) return false;
39293
+ if (!isNodeOfType(child, "CallExpression")) return;
39294
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39295
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39296
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39297
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39298
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39299
+ matchedCall = child;
39300
+ return false;
39301
+ }
39302
+ });
39303
+ return matchedCall;
39304
+ };
38436
39305
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38437
39306
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38438
39307
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38449,6 +39318,16 @@ const preferUseSyncExternalStore = defineRule({
38449
39318
  severity: "warn",
38450
39319
  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
39320
  create: (context) => {
39321
+ let cachedStoreIndex = null;
39322
+ const storeIndexFor = (node) => {
39323
+ if (cachedStoreIndex) return cachedStoreIndex;
39324
+ const programRoot = findProgramRoot(node);
39325
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39326
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39327
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39328
+ };
39329
+ return cachedStoreIndex;
39330
+ };
38452
39331
  const checkComponent = (componentBody) => {
38453
39332
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38454
39333
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38486,6 +39365,7 @@ const preferUseSyncExternalStore = defineRule({
38486
39365
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38487
39366
  if (!useStateInitializer) continue;
38488
39367
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39368
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38489
39369
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38490
39370
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38491
39371
  context.report({
@@ -38493,14 +39373,47 @@ const preferUseSyncExternalStore = defineRule({
38493
39373
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38494
39374
  });
38495
39375
  }
39376
+ checkModuleStoreShape(componentBody, useStateBindings);
39377
+ };
39378
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39379
+ const storeIndex = storeIndexFor(componentBody);
39380
+ if (storeIndex.mutableBindingNames.size === 0) return;
39381
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39382
+ const snapshotBindings = useStateBindings.map((binding) => ({
39383
+ binding,
39384
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39385
+ })).filter((candidate) => candidate.storeName !== null);
39386
+ if (snapshotBindings.length === 0) return;
39387
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39388
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39389
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39390
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39391
+ const depsNode = effectCall.arguments[1];
39392
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39393
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39394
+ const callback = getEffectCallback(effectCall);
39395
+ if (!callback || !isFunctionLike$1(callback)) continue;
39396
+ for (const { binding, storeName } of snapshotBindings) {
39397
+ if (reportedDeclarators.has(binding.declarator)) continue;
39398
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39399
+ reportedDeclarators.add(binding.declarator);
39400
+ context.report({
39401
+ node: binding.declarator,
39402
+ 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.`
39403
+ });
39404
+ }
39405
+ }
38496
39406
  };
38497
39407
  return {
38498
39408
  FunctionDeclaration(node) {
38499
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39409
+ const functionName = node.id?.name;
39410
+ if (!functionName) return;
39411
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38500
39412
  checkComponent(node.body);
38501
39413
  },
38502
39414
  VariableDeclarator(node) {
38503
- if (!isComponentAssignment(node)) return;
39415
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39416
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38504
39417
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38505
39418
  checkComponent(node.init.body);
38506
39419
  }
@@ -39110,7 +40023,7 @@ const queryNoVoidQueryFn = defineRule({
39110
40023
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39111
40024
  const body = queryFnValue.body;
39112
40025
  if (!isNodeOfType(body, "BlockStatement")) return;
39113
- if ((body.body ?? []).length === 0) context.report({
40026
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39114
40027
  node: queryFnProperty,
39115
40028
  message: "This empty queryFn caches undefined, so the component never gets data."
39116
40029
  });
@@ -39617,17 +40530,6 @@ const renderingHoistJsx = defineRule({
39617
40530
  }
39618
40531
  });
39619
40532
  //#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
40533
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39632
40534
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39633
40535
  {
@@ -39636,19 +40538,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39636
40538
  },
39637
40539
  {
39638
40540
  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"
40541
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39640
40542
  },
39641
40543
  {
39642
40544
  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"
40545
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39644
40546
  },
39645
40547
  {
39646
40548
  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"
40549
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39648
40550
  },
39649
40551
  {
39650
40552
  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"
40553
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39652
40554
  }
39653
40555
  ];
39654
40556
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39660,54 +40562,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39660
40562
  }
39661
40563
  return null;
39662
40564
  };
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
40565
  const isYearOnlyDateRead = (dateNode) => {
39712
40566
  const member = dateNode.parent;
39713
40567
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -39731,23 +40585,6 @@ const isInsideMotionTransitionAttribute = (node) => {
39731
40585
  }
39732
40586
  return false;
39733
40587
  };
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
40588
  const renderingHydrationMismatchTime = defineRule({
39752
40589
  id: "rendering-hydration-mismatch-time",
39753
40590
  title: "Time or random value in JSX",
@@ -39795,6 +40632,35 @@ const renderingHydrationMismatchTime = defineRule({
39795
40632
  }
39796
40633
  });
39797
40634
  //#endregion
40635
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40636
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40637
+ "toLocaleString",
40638
+ "toLocaleDateString",
40639
+ "toLocaleTimeString",
40640
+ "getTimezoneOffset"
40641
+ ]);
40642
+ const containsLocaleEnvironmentRead = (expression) => {
40643
+ let readsLocaleEnvironment = false;
40644
+ walkAst(expression, (child) => {
40645
+ if (readsLocaleEnvironment) return false;
40646
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40647
+ if (child.object.name === "Intl") {
40648
+ readsLocaleEnvironment = true;
40649
+ return false;
40650
+ }
40651
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40652
+ readsLocaleEnvironment = true;
40653
+ return false;
40654
+ }
40655
+ }
40656
+ 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)) {
40657
+ readsLocaleEnvironment = true;
40658
+ return false;
40659
+ }
40660
+ });
40661
+ return readsLocaleEnvironment;
40662
+ };
40663
+ //#endregion
39798
40664
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
39799
40665
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
39800
40666
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -39860,14 +40726,15 @@ const renderingHydrationNoFlicker = defineRule({
39860
40726
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
39861
40727
  const callback = getEffectCallback(node);
39862
40728
  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;
40729
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
40730
+ if (bodyStatements.length !== 1) return;
39865
40731
  const soleStatement = bodyStatements[0];
39866
40732
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
39867
40733
  const expression = soleStatement.expression;
39868
40734
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
39869
40735
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
39870
40736
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
40737
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
39871
40738
  context.report({
39872
40739
  node,
39873
40740
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40686,6 +41553,9 @@ const rerenderFunctionalSetstate = defineRule({
40686
41553
  } })
40687
41554
  });
40688
41555
  //#endregion
41556
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41557
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41558
+ //#endregion
40689
41559
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40690
41560
  const rerenderLazyRefInit = defineRule({
40691
41561
  id: "rerender-lazy-ref-init",
@@ -40696,7 +41566,7 @@ const rerenderLazyRefInit = defineRule({
40696
41566
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40697
41567
  create: (context) => ({ CallExpression(node) {
40698
41568
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40699
- const initializer = node.arguments[0];
41569
+ const initializer = stripParenExpression(node.arguments[0]);
40700
41570
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40701
41571
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40702
41572
  if (!isPlainCall && !isNewCall) return;
@@ -40704,6 +41574,7 @@ const rerenderLazyRefInit = defineRule({
40704
41574
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40705
41575
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40706
41576
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
41577
+ if (isTrivialBuiltInConstruction(initializer)) return;
40707
41578
  if (isPlainCall && isReactHookName(calleeName)) return;
40708
41579
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40709
41580
  context.report({
@@ -40736,26 +41607,14 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40736
41607
  "getUTCMilliseconds",
40737
41608
  "valueOf"
40738
41609
  ]);
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
41610
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40752
41611
  const findEagerInitializerCall = (expression, depth = 0) => {
40753
41612
  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 ?? []) {
41613
+ const innerExpression = stripParenExpression(expression);
41614
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41615
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41616
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41617
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40759
41618
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
40760
41619
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
40761
41620
  if (spreadCall) return spreadCall;
@@ -40778,7 +41637,7 @@ const rerenderLazyStateInit = defineRule({
40778
41637
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40779
41638
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40780
41639
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40781
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41640
+ if (isTrivialBuiltInConstruction(initializer)) return;
40782
41641
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
40783
41642
  if (isReactHookName(calleeName)) return;
40784
41643
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41445,7 +42304,7 @@ const rnAnimationReactionAsDerived = defineRule({
41445
42304
  const body = reactionFn.body;
41446
42305
  let singleAssignment = null;
41447
42306
  if (isNodeOfType(body, "BlockStatement")) {
41448
- const statements = body.body ?? [];
42307
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41449
42308
  if (statements.length !== 1) return;
41450
42309
  const onlyStatement = statements[0];
41451
42310
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41491,7 +42350,7 @@ const rnBottomSheetPreferNative = defineRule({
41491
42350
  });
41492
42351
  //#endregion
41493
42352
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41494
- const EMPTY_VISITORS$4 = {};
42353
+ const EMPTY_VISITORS$5 = {};
41495
42354
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41496
42355
  const DETOX_ELEMENT_ACTIONS = new Set([
41497
42356
  "tap",
@@ -41519,7 +42378,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41519
42378
  "catch",
41520
42379
  "finally"
41521
42380
  ]);
41522
- const findChainRoot = (node) => {
42381
+ const findChainRoot = (wrappedNode) => {
42382
+ const node = stripParenExpression(wrappedNode);
41523
42383
  if (isNodeOfType(node, "CallExpression")) {
41524
42384
  if (isNodeOfType(node.callee, "Identifier")) return {
41525
42385
  calleeName: node.callee.name,
@@ -41551,7 +42411,7 @@ const rnDetoxMissingAwait = defineRule({
41551
42411
  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
42412
  create: (context) => {
41553
42413
  const filename = normalizeFilename(context.filename ?? "");
41554
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
42414
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41555
42415
  return { ExpressionStatement(node) {
41556
42416
  const expression = node.expression;
41557
42417
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -42107,20 +42967,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42107
42967
  };
42108
42968
  const isReactNativeDimensionsCallee = (node, callee) => {
42109
42969
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42110
- if (isNodeOfType(callee.object, "Identifier")) {
42111
- const localName = callee.object.name;
42970
+ const receiver = stripParenExpression(callee.object);
42971
+ if (isNodeOfType(receiver, "Identifier")) {
42972
+ const localName = receiver.name;
42112
42973
  const binding = findVariableInitializer(node, localName);
42113
42974
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42114
42975
  return localName === "Dimensions";
42115
42976
  }
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);
42977
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
42978
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
42979
+ const rootName = getRootIdentifierName(receiver.object);
42119
42980
  if (rootName === null) return false;
42120
42981
  const importBinding = getImportBindingForName(node, rootName);
42121
42982
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42122
42983
  }
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";
42984
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42124
42985
  return false;
42125
42986
  };
42126
42987
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42526,7 +43387,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42526
43387
  };
42527
43388
  //#endregion
42528
43389
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42529
- const EMPTY_VISITORS$3 = {};
43390
+ const EMPTY_VISITORS$4 = {};
42530
43391
  const reportLegacyShadowProperties = (objectExpression, context) => {
42531
43392
  const legacyShadowPropertyNames = [];
42532
43393
  for (const property of objectExpression.properties ?? []) {
@@ -42549,7 +43410,7 @@ const rnNoLegacyShadowStyles = defineRule({
42549
43410
  severity: "warn",
42550
43411
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42551
43412
  create: (context) => {
42552
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
43413
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42553
43414
  return {
42554
43415
  JSXAttribute(node) {
42555
43416
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -42564,7 +43425,8 @@ const rnNoLegacyShadowStyles = defineRule({
42564
43425
  },
42565
43426
  CallExpression(node) {
42566
43427
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42567
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43428
+ const receiver = stripParenExpression(node.callee.object);
43429
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42568
43430
  if (!isMemberProperty(node.callee, "create")) return;
42569
43431
  const stylesArgument = node.arguments?.[0];
42570
43432
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43414,7 +44276,7 @@ const rnNoSetNativeProps = defineRule({
43414
44276
  const callee = node.callee;
43415
44277
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43416
44278
  if (!isNodeOfType(callee, "MemberExpression")) return;
43417
- if (!isStaticMemberNamed(callee.object, "current")) return;
44279
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43418
44280
  context.report({
43419
44281
  node,
43420
44282
  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 +44332,7 @@ const isExpoManagedFileActive = (context) => {
43470
44332
  };
43471
44333
  //#endregion
43472
44334
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43473
- const EMPTY_VISITORS$2 = {};
44335
+ const EMPTY_VISITORS$3 = {};
43474
44336
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43475
44337
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43476
44338
  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 +44366,7 @@ const rnPreferExpoImage = defineRule({
43504
44366
  severity: "warn",
43505
44367
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43506
44368
  create: (context) => {
43507
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
44369
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43508
44370
  const flaggedImports = [];
43509
44371
  const assetBindingNames = /* @__PURE__ */ new Set();
43510
44372
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -43642,14 +44504,15 @@ const analyzeGestureChain = (expression) => {
43642
44504
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43643
44505
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43644
44506
  const methodName = callee.property.name;
43645
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44507
+ const receiver = stripParenExpression(callee.object);
44508
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43646
44509
  factoryName: methodName,
43647
44510
  chainMethodNames,
43648
44511
  numberOfTapsArgument
43649
44512
  };
43650
44513
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43651
44514
  chainMethodNames.push(methodName);
43652
- cursor = callee.object;
44515
+ cursor = receiver;
43653
44516
  }
43654
44517
  return null;
43655
44518
  };
@@ -43991,7 +44854,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
43991
44854
  });
43992
44855
  //#endregion
43993
44856
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
43994
- const EMPTY_VISITORS$1 = {};
44857
+ const EMPTY_VISITORS$2 = {};
43995
44858
  const IOS_SHADOW_KEYS = new Set([
43996
44859
  "shadowColor",
43997
44860
  "shadowOffset",
@@ -44077,7 +44940,7 @@ const rnStylePreferBoxShadow = defineRule({
44077
44940
  severity: "warn",
44078
44941
  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
44942
  create: (context) => {
44080
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
44943
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44081
44944
  return {
44082
44945
  JSXAttribute(node) {
44083
44946
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44174,9 +45037,9 @@ const roleHasRequiredAriaProps = defineRule({
44174
45037
  if (!HTML_TAGS.has(elementType)) return;
44175
45038
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44176
45039
  if (!roleAttribute) return;
44177
- const roleValue = getJsxPropStringValue(roleAttribute);
44178
- if (roleValue === null) return;
44179
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
45040
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
45041
+ if (roleCandidates === null) return;
45042
+ const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44180
45043
  for (const role of roles) {
44181
45044
  const required = ROLE_REQUIRED_PROPS.get(role);
44182
45045
  if (!required) continue;
@@ -47293,7 +48156,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47293
48156
  };
47294
48157
  //#endregion
47295
48158
  //#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.`;
48159
+ const buildMessageDefault = (roles, propName) => {
48160
+ 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.`;
48161
+ };
47297
48162
  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
48163
  const roleSupportsAriaProps = defineRule({
47299
48164
  id: "role-supports-aria-props",
@@ -47313,6 +48178,8 @@ const roleSupportsAriaProps = defineRule({
47313
48178
  const propName = propRawName.toLowerCase();
47314
48179
  if (!propName.startsWith("aria-")) continue;
47315
48180
  if (!ARIA_PROPERTIES.has(propName)) continue;
48181
+ const attributeValue = attributeNode.value;
48182
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47316
48183
  (ariaAttributes ??= []).push({
47317
48184
  attribute,
47318
48185
  propName
@@ -47321,17 +48188,21 @@ const roleSupportsAriaProps = defineRule({
47321
48188
  if (!ariaAttributes) return;
47322
48189
  const elementType = getElementType(node, context.settings);
47323
48190
  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;
48191
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
48192
+ if (roleCandidates === null || roleCandidates.length === 0) return;
48193
+ const supportedSets = [];
48194
+ for (const role of roleCandidates) {
48195
+ if (!VALID_ARIA_ROLES.has(role)) return;
48196
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
48197
+ if (!supported) return;
48198
+ supportedSets.push(supported);
48199
+ }
47327
48200
  const isImplicit = !roleAttribute;
47328
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47329
- if (!supported) return;
47330
48201
  for (const { attribute, propName } of ariaAttributes) {
47331
- if (supported.has(propName)) continue;
48202
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47332
48203
  context.report({
47333
48204
  node: attribute,
47334
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
48205
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47335
48206
  });
47336
48207
  }
47337
48208
  } })
@@ -47683,11 +48554,19 @@ const isUseEffectEventSymbol = (symbol) => {
47683
48554
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47684
48555
  return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47685
48556
  };
47686
- const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47687
- const isNonReactEffectEventSymbol = (symbol, contextNode) => {
48557
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
48558
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
48559
+ return Boolean(symbol && symbol.kind !== "import");
48560
+ };
48561
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
48562
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
48563
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
48564
+ return false;
48565
+ };
48566
+ const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47688
48567
  const initializer = symbol.initializer;
47689
48568
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47690
- return isNonReactEffectEventCallee(initializer.callee, contextNode);
48569
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47691
48570
  };
47692
48571
  const findEnclosingComponentOrHookFunction = (node) => {
47693
48572
  let current = node.parent;
@@ -47766,7 +48645,7 @@ const rulesOfHooks = defineRule({
47766
48645
  const hookContext = isHookCall$1(node, context.scopes, settings);
47767
48646
  if (!hookContext) return;
47768
48647
  const { hookName } = hookContext;
47769
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
48648
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47770
48649
  context.report({
47771
48650
  node: node.callee,
47772
48651
  message: buildEffectEventPassedDownMessage()
@@ -47879,7 +48758,7 @@ const rulesOfHooks = defineRule({
47879
48758
  Identifier(node) {
47880
48759
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47881
48760
  if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47882
- if (isNonReactEffectEventSymbol(symbol, node)) return;
48761
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
47883
48762
  if (!isSameComponentOrHookScope(symbol, node)) return;
47884
48763
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47885
48764
  context.report({
@@ -48027,7 +48906,8 @@ const serverAfterNonblocking = defineRule({
48027
48906
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48028
48907
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48029
48908
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48030
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
48909
+ const receiver = stripParenExpression(node.callee.object);
48910
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48031
48911
  if (!objectName) return;
48032
48912
  const methodName = node.callee.property.name;
48033
48913
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48703,7 +49583,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48703
49583
  };
48704
49584
  const ascendMemberChain = (referenceIdentifier) => {
48705
49585
  let chainTip = referenceIdentifier;
48706
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49586
+ while (chainTip.parent) {
49587
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49588
+ chainTip = chainTip.parent;
49589
+ continue;
49590
+ }
49591
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49592
+ chainTip = chainTip.parent;
49593
+ continue;
49594
+ }
49595
+ break;
49596
+ }
48707
49597
  return chainTip;
48708
49598
  };
48709
49599
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -48728,7 +49618,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
48728
49618
  const callee = callExpression.callee;
48729
49619
  if (isNodeOfType(callee, "MemberExpression")) {
48730
49620
  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);
49621
+ const calleeReceiver = stripParenExpression(callee.object);
49622
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48732
49623
  }
48733
49624
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
48734
49625
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49458,7 +50349,7 @@ const walkServerFnChain = (outerNode) => {
49458
50349
  };
49459
50350
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49460
50351
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49461
- let currentNode = outerNode.callee.object;
50352
+ let currentNode = stripParenExpression(outerNode.callee.object);
49462
50353
  while (isNodeOfType(currentNode, "CallExpression")) {
49463
50354
  const calleeName = getCalleeName$2(currentNode);
49464
50355
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49469,7 +50360,7 @@ const walkServerFnChain = (outerNode) => {
49469
50360
  }
49470
50361
  }
49471
50362
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49472
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50363
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49473
50364
  else break;
49474
50365
  }
49475
50366
  return result;
@@ -50122,7 +51013,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50122
51013
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50123
51014
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50124
51015
  if (methodName) methodNames.unshift(methodName);
50125
- currentNode = currentNode.callee.object;
51016
+ currentNode = stripParenExpression(currentNode.callee.object);
50126
51017
  }
50127
51018
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50128
51019
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53204,6 +54095,18 @@ const reactDoctorRules = [
53204
54095
  category: "Bugs"
53205
54096
  }
53206
54097
  },
54098
+ {
54099
+ key: "react-doctor/no-locale-format-in-render",
54100
+ id: "no-locale-format-in-render",
54101
+ source: "react-doctor",
54102
+ originallyExternal: false,
54103
+ rule: {
54104
+ ...noLocaleFormatInRender,
54105
+ framework: "global",
54106
+ category: "Bugs",
54107
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54108
+ }
54109
+ },
53207
54110
  {
53208
54111
  key: "react-doctor/no-long-transition-duration",
53209
54112
  id: "no-long-transition-duration",
@@ -55451,6 +56354,34 @@ const reactDoctorRules = [
55451
56354
  ];
55452
56355
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55453
56356
  //#endregion
56357
+ //#region src/plugin/utils/is-next-file.ts
56358
+ const isNextFileActive = (context) => {
56359
+ const rawFilename = context.filename;
56360
+ if (!rawFilename) return true;
56361
+ const filename = normalizeFilename(rawFilename);
56362
+ const manifest = readNearestPackageManifest(filename);
56363
+ if (!manifest) return true;
56364
+ if (declaresDependency(manifest, "next")) return true;
56365
+ if (!declaresAnyDependency(manifest)) return true;
56366
+ const packageDirectory = findNearestPackageDirectory(filename);
56367
+ const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
56368
+ if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
56369
+ return true;
56370
+ };
56371
+ //#endregion
56372
+ //#region src/plugin/utils/wrap-nextjs-rule.ts
56373
+ const EMPTY_VISITORS$1 = {};
56374
+ const wrapNextjsRule = (rule) => {
56375
+ const innerCreate = rule.create.bind(rule);
56376
+ return {
56377
+ ...rule,
56378
+ create: (context) => {
56379
+ if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
56380
+ return innerCreate(context);
56381
+ }
56382
+ };
56383
+ };
56384
+ //#endregion
55454
56385
  //#region src/plugin/utils/wrap-react-native-rule.ts
55455
56386
  const EMPTY_VISITORS = {};
55456
56387
  const wrapReactNativeRule = (rule) => {
@@ -55920,9 +56851,14 @@ const wrapWithSemanticContext = (rule) => ({
55920
56851
  });
55921
56852
  //#endregion
55922
56853
  //#region src/plugin/react-doctor-plugin.ts
56854
+ const applyFrameworkGate = (rule) => {
56855
+ if (rule.framework === "react-native") return wrapReactNativeRule(rule);
56856
+ if (rule.framework === "nextjs") return wrapNextjsRule(rule);
56857
+ return rule;
56858
+ };
55923
56859
  const applyFrameworkRuleWrappers = (registry) => {
55924
56860
  const wrapped = {};
55925
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
56861
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55926
56862
  return wrapped;
55927
56863
  };
55928
56864
  const plugin = {