oxlint-plugin-react-doctor 0.7.2-dev.f10f9ca → 0.7.2-dev.fb8ffb0

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 +0 -46
  2. package/dist/index.js +485 -1408
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -532,12 +532,6 @@ 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
- ]);
541
535
  const MUTABLE_GLOBAL_ROOTS = new Set([
542
536
  "location",
543
537
  "window",
@@ -693,10 +687,7 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
693
687
  "unwatch",
694
688
  "unlisten",
695
689
  "unsub",
696
- "abort",
697
- "disconnect",
698
- "unobserve",
699
- "close"
690
+ "abort"
700
691
  ]);
701
692
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
702
693
  "remove",
@@ -2257,39 +2248,6 @@ const anchorHasContent = defineRule({
2257
2248
  }
2258
2249
  });
2259
2250
  //#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
2293
2251
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2294
2252
  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).";
2295
2253
  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.";
@@ -2317,14 +2275,30 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2317
2275
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2318
2276
  const isInvalidHref = (value, validHrefs) => {
2319
2277
  if (validHrefs.has(value)) return false;
2320
- const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
- return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2278
+ return value === "" || value === "#" || value === "javascript:void(0)";
2279
+ };
2280
+ const getStaticHrefValue = (value) => {
2281
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
2282
+ if (isNodeOfType(value, "JSXExpressionContainer")) {
2283
+ const expression = value.expression;
2284
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
2285
+ if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
2286
+ }
2287
+ return null;
2322
2288
  };
2323
- const isNullishOrFragmentHref = (value) => {
2289
+ const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
2290
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
2324
2291
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2325
2292
  const expression = value.expression;
2326
2293
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2327
- if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2294
+ if (isNodeOfType(expression, "Literal")) {
2295
+ if (expression.value === null) return true;
2296
+ if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2297
+ }
2298
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2299
+ const staticValue = getStaticTemplateLiteralValue(expression);
2300
+ return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2301
+ }
2328
2302
  }
2329
2303
  if (isNodeOfType(value, "JSXFragment")) return true;
2330
2304
  return false;
@@ -2357,10 +2331,9 @@ const anchorIsValid = defineRule({
2357
2331
  });
2358
2332
  return;
2359
2333
  }
2360
- const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2361
- if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2334
+ if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2362
2335
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2363
- if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2336
+ if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2364
2337
  context.report({
2365
2338
  node: node.name,
2366
2339
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3416,25 +3389,6 @@ const ariaRole = defineRule({
3416
3389
  if (!roleAttribute) return;
3417
3390
  const elementType = getElementType(node, context.settings);
3418
3391
  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
- };
3438
3392
  const value = roleAttribute.value;
3439
3393
  if (!value) {
3440
3394
  context.report({
@@ -3451,7 +3405,22 @@ const ariaRole = defineRule({
3451
3405
  });
3452
3406
  return;
3453
3407
  }
3454
- reportFirstInvalidCandidate([value.value]);
3408
+ const stringValue = value.value;
3409
+ if (stringValue.trim().length === 0) {
3410
+ context.report({
3411
+ node: roleAttribute,
3412
+ message: buildBaseMessage("")
3413
+ });
3414
+ return;
3415
+ }
3416
+ const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
3417
+ for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3418
+ context.report({
3419
+ node: roleAttribute,
3420
+ message: buildBaseMessage(` \`${token}\` is not one.`)
3421
+ });
3422
+ return;
3423
+ }
3455
3424
  return;
3456
3425
  }
3457
3426
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3472,11 +3441,6 @@ const ariaRole = defineRule({
3472
3441
  });
3473
3442
  return;
3474
3443
  }
3475
- const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3476
- if (resolvedCandidates !== null) {
3477
- reportFirstInvalidCandidate(resolvedCandidates);
3478
- return;
3479
- }
3480
3444
  return;
3481
3445
  }
3482
3446
  context.report({
@@ -5193,7 +5157,7 @@ const authTokenInWebStorage = defineRule({
5193
5157
  const callee = node.callee;
5194
5158
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5195
5159
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5196
- if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5160
+ if (!isWebStorageObject(callee.object)) return;
5197
5161
  const keyArgument = node.arguments?.[0];
5198
5162
  if (!keyArgument) return;
5199
5163
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6133,21 +6097,19 @@ const clickjackingRedirectRisk = defineRule({
6133
6097
  })
6134
6098
  });
6135
6099
  //#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
6145
6100
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6146
6101
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6147
6102
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6148
6103
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6149
6104
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6150
- const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6105
+ const isJsonStringifyCall = (node) => {
6106
+ if (!isNodeOfType(node, "CallExpression")) return false;
6107
+ if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6108
+ if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6109
+ if (node.callee.object.name !== "JSON") return false;
6110
+ if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6111
+ return node.callee.property.name === "stringify";
6112
+ };
6151
6113
  const resolveStringKey = (keyArg, context) => {
6152
6114
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6153
6115
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6166,9 +6128,8 @@ const clientLocalstorageNoVersion = defineRule({
6166
6128
  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.",
6167
6129
  create: (context) => ({ CallExpression(node) {
6168
6130
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6169
- const receiver = stripParenExpression(node.callee.object);
6170
- if (!isNodeOfType(receiver, "Identifier")) return;
6171
- if (!STORAGE_OBJECTS.has(receiver.name)) return;
6131
+ if (!isNodeOfType(node.callee.object, "Identifier")) return;
6132
+ if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
6172
6133
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6173
6134
  if (node.callee.property.name !== "setItem") return;
6174
6135
  const keyArg = node.arguments?.[0];
@@ -6181,7 +6142,7 @@ const clientLocalstorageNoVersion = defineRule({
6181
6142
  if (!isJsonStringifyCall(valueArg)) return;
6182
6143
  context.report({
6183
6144
  node: keyArg,
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").`
6145
+ message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6185
6146
  });
6186
6147
  } })
6187
6148
  });
@@ -7662,96 +7623,6 @@ const displayName = defineRule({
7662
7623
  }
7663
7624
  });
7664
7625
  //#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
7755
7626
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7756
7627
  const walkInsideStatementBlocks = (node, visitor) => {
7757
7628
  if (!node || typeof node !== "object") return;
@@ -7903,17 +7774,6 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7903
7774
  };
7904
7775
  //#endregion
7905
7776
  //#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
- };
7917
7777
  const findSubscribeLikeUsages = (callback) => {
7918
7778
  const usages = [];
7919
7779
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7925,14 +7785,6 @@ const findSubscribeLikeUsages = (callback) => {
7925
7785
  }
7926
7786
  walkAst(callback, (child) => {
7927
7787
  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
- }
7936
7788
  if (!isNodeOfType(child, "CallExpression")) return;
7937
7789
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7938
7790
  usages.push({
@@ -7942,7 +7794,7 @@ const findSubscribeLikeUsages = (callback) => {
7942
7794
  });
7943
7795
  return;
7944
7796
  }
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({
7797
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7946
7798
  kind: "subscribe",
7947
7799
  node: child,
7948
7800
  resourceName: child.callee.property.name
@@ -7958,13 +7810,6 @@ const collectCleanupBindings = (effectCallback) => {
7958
7810
  };
7959
7811
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7960
7812
  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
- });
7968
7813
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7969
7814
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7970
7815
  for (const declarator of child.declarations ?? []) {
@@ -7972,12 +7817,7 @@ const collectCleanupBindings = (effectCallback) => {
7972
7817
  const bindingName = declarator.id.name;
7973
7818
  bindings.effectScopeVariableNames.add(bindingName);
7974
7819
  const init = declarator.init;
7975
- if (!init) continue;
7976
- if (isSocketConstruction(init)) {
7977
- bindings.subscriptionNames.add(bindingName);
7978
- continue;
7979
- }
7980
- if (!isNodeOfType(init, "CallExpression")) continue;
7820
+ if (!init || !isNodeOfType(init, "CallExpression")) continue;
7981
7821
  if (isSubscribeLikeCallExpression(init)) {
7982
7822
  bindings.subscriptionNames.add(bindingName);
7983
7823
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -8007,22 +7847,6 @@ const getRangeStart = (node) => {
8007
7847
  const rangeStart = node.range?.[0];
8008
7848
  return typeof rangeStart === "number" ? rangeStart : null;
8009
7849
  };
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
- };
8026
7850
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
8027
7851
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
8028
7852
  const returnStart = getRangeStart(returnStatement);
@@ -8045,177 +7869,26 @@ const effectHasCleanupReturn = (callback, usages) => {
8045
7869
  });
8046
7870
  return didFindCleanupReturn;
8047
7871
  };
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
- };
8172
7872
  const effectNeedsCleanup = defineRule({
8173
7873
  id: "effect-needs-cleanup",
8174
7874
  title: "Effect subscription or timer never cleaned up",
8175
7875
  severity: "error",
8176
7876
  tags: ["test-noise"],
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
- }
7877
+ recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
7878
+ create: (context) => ({ CallExpression(node) {
7879
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7880
+ const callback = getEffectCallback(node);
7881
+ if (!callback) return;
7882
+ const usages = findSubscribeLikeUsages(callback);
7883
+ if (usages.length === 0) return;
7884
+ if (effectHasCleanupReturn(callback, usages)) return;
7885
+ const firstUsage = usages[0];
7886
+ const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7887
+ context.report({
7888
+ node,
7889
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7890
+ });
7891
+ } })
8219
7892
  });
8220
7893
  //#endregion
8221
7894
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8774,6 +8447,17 @@ const closureCaptures = (functionNode, scopes) => {
8774
8447
  return computedCaptures;
8775
8448
  };
8776
8449
  //#endregion
8450
+ //#region src/plugin/utils/is-react-hook-name.ts
8451
+ const isReactHookName = (name) => {
8452
+ if (!name.startsWith("use")) return false;
8453
+ if (name.length === 3) return true;
8454
+ const fourthCharacter = name.charCodeAt(3);
8455
+ return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8456
+ };
8457
+ //#endregion
8458
+ //#region src/plugin/utils/is-react-component-or-hook-name.ts
8459
+ const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8460
+ //#endregion
8777
8461
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8778
8462
  const reactHocCalleeName = (callee) => {
8779
8463
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -8987,8 +8671,8 @@ const isAstDescendant = (inner, outer) => {
8987
8671
  * One cohesive concept: "given a captured symbol, is its value
8988
8672
  * structurally stable across re-renders (and therefore unnecessary
8989
8673
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8990
- * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
8991
- * multiple sites — extracting
8674
+ * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
8675
+ * `isRecursiveInitializerCapture` at multiple sites — extracting
8992
8676
  * them lets the rule body stay focused on the diff-the-captured-vs-
8993
8677
  * declared logic.
8994
8678
  *
@@ -9044,6 +8728,11 @@ const symbolHasStableHookOrigin = (symbol) => {
9044
8728
  }
9045
8729
  return false;
9046
8730
  };
8731
+ const symbolHasUseEffectEventOrigin = (symbol) => {
8732
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
8733
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
8734
+ return getHookName(initializer.callee) === "useEffectEvent";
8735
+ };
9047
8736
  const getFunctionValueNode = (symbol) => {
9048
8737
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
9049
8738
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -9129,37 +8818,6 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9129
8818
  };
9130
8819
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
9131
8820
  //#endregion
9132
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
9133
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9134
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
9135
- if (importSource === null) return false;
9136
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9137
- };
9138
- //#endregion
9139
- //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9140
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9141
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9142
- return Boolean(symbol && symbol.kind !== "import");
9143
- };
9144
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9145
- if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9146
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9147
- return false;
9148
- };
9149
- //#endregion
9150
- //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9151
- const getUseEffectEventCalleeName = (callee) => {
9152
- if (isNodeOfType(callee, "Identifier")) return callee.name;
9153
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9154
- return null;
9155
- };
9156
- const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9157
- const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9158
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9159
- if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9160
- return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9161
- };
9162
- //#endregion
9163
8821
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
9164
8822
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
9165
8823
  "useEffect",
@@ -9810,7 +9468,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9810
9468
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9811
9469
  if (isNodeOfType(stripped, "Identifier")) {
9812
9470
  const depSymbol = context.scopes.symbolFor(stripped);
9813
- if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9471
+ if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
9814
9472
  context.report({
9815
9473
  node: elementNode,
9816
9474
  message: buildEffectEventDepMessage()
@@ -10236,10 +9894,7 @@ const PRAGMA = "React";
10236
9894
  const isReactFunctionCall = (node, expectedCall) => {
10237
9895
  if (!isNodeOfType(node, "CallExpression")) return false;
10238
9896
  if (getCalleeName$2(node) !== expectedCall) return false;
10239
- if (isNodeOfType(node.callee, "MemberExpression")) {
10240
- const receiver = stripParenExpression(node.callee.object);
10241
- return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10242
- }
9897
+ if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10243
9898
  return true;
10244
9899
  };
10245
9900
  //#endregion
@@ -11404,8 +11059,8 @@ const interactiveSupportsFocus = defineRule({
11404
11059
  if (node.attributes.length === 0) return;
11405
11060
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11406
11061
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11407
- const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11408
- if (roleCandidates === null || roleCandidates.length === 0) return;
11062
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11063
+ if (!role) return;
11409
11064
  let hasInteractiveHandler = false;
11410
11065
  for (const attribute of node.attributes) {
11411
11066
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11419,16 +11074,11 @@ const interactiveSupportsFocus = defineRule({
11419
11074
  const elementType = getElementType(node, context.settings);
11420
11075
  if (!HTML_TAGS.has(elementType)) return;
11421
11076
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11077
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11078
+ if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11422
11079
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11423
- const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11424
- for (const role of roleCandidates) {
11425
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11426
- if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11427
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11428
- }
11429
- const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11430
- const roleDisplay = roleCandidates.join("' / '");
11431
- const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11080
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11081
+ const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
11432
11082
  context.report({
11433
11083
  node,
11434
11084
  message
@@ -11617,6 +11267,16 @@ const collectHandlerReferencedNames = (root) => {
11617
11267
  return names;
11618
11268
  };
11619
11269
  //#endregion
11270
+ //#region src/plugin/utils/find-enclosing-function.ts
11271
+ const findEnclosingFunction = (node) => {
11272
+ let cursor = node.parent;
11273
+ while (cursor) {
11274
+ if (isFunctionLike$1(cursor)) return cursor;
11275
+ cursor = cursor.parent ?? null;
11276
+ }
11277
+ return null;
11278
+ };
11279
+ //#endregion
11620
11280
  //#region src/plugin/utils/get-function-binding-name.ts
11621
11281
  const getFunctionBindingIdentifier = (functionNode) => {
11622
11282
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -12316,15 +11976,14 @@ const jsCacheStorage = defineRule({
12316
11976
  "ArrowFunctionExpression:exit": exitFunctionScope,
12317
11977
  CallExpression(node) {
12318
11978
  if (!isMemberProperty(node.callee, "getItem")) return;
12319
- const receiver = stripParenExpression(node.callee.object);
12320
- if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
11979
+ if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12321
11980
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
12322
11981
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
12323
11982
  const storageKey = String(node.arguments[0].value);
12324
11983
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
12325
11984
  storageReadCounts.set(storageKey, readCount);
12326
11985
  if (readCount === 2) {
12327
- const storageName = receiver.name;
11986
+ const storageName = node.callee.object.name;
12328
11987
  context.report({
12329
11988
  node,
12330
11989
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -12338,16 +11997,13 @@ const jsCacheStorage = defineRule({
12338
11997
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
12339
11998
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
12340
11999
  const callee = callExpression.callee;
12341
- if (isNodeOfType(callee, "MemberExpression")) {
12342
- const receiver = stripParenExpression(callee.object);
12343
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12344
- if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12345
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12346
- return true;
12347
- }
12348
- return false;
12349
- }
12000
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12350
12001
  if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12002
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12003
+ const receiver = callee.object;
12004
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12005
+ return true;
12006
+ }
12351
12007
  return false;
12352
12008
  };
12353
12009
  const isChainPassThroughCall = (callExpression) => {
@@ -12359,7 +12015,10 @@ const isChainPassThroughCall = (callExpression) => {
12359
12015
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12360
12016
  let cursor = receiverNode;
12361
12017
  while (cursor) {
12362
- cursor = stripParenExpression(cursor);
12018
+ if (isNodeOfType(cursor, "ChainExpression")) {
12019
+ cursor = cursor.expression;
12020
+ continue;
12021
+ }
12363
12022
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12364
12023
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12365
12024
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12435,7 +12094,10 @@ const isStringSplitRootedChain = (receiverNode) => {
12435
12094
  let hops = 0;
12436
12095
  while (cursor && hops < 12) {
12437
12096
  hops += 1;
12438
- cursor = stripParenExpression(cursor);
12097
+ if (isNodeOfType(cursor, "ChainExpression")) {
12098
+ cursor = cursor.expression;
12099
+ continue;
12100
+ }
12439
12101
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12440
12102
  const callee = cursor.callee;
12441
12103
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12459,7 +12121,10 @@ const isSmallLiteralArray = (node) => {
12459
12121
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12460
12122
  let cursor = receiverNode;
12461
12123
  while (cursor) {
12462
- cursor = stripParenExpression(cursor);
12124
+ if (isNodeOfType(cursor, "ChainExpression")) {
12125
+ cursor = cursor.expression;
12126
+ continue;
12127
+ }
12463
12128
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12464
12129
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12465
12130
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12524,7 +12189,7 @@ const jsCombineIterations = defineRule({
12524
12189
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12525
12190
  const outerMethod = node.callee.property.name;
12526
12191
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12527
- const innerCall = stripParenExpression(node.callee.object);
12192
+ const innerCall = node.callee.object;
12528
12193
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12529
12194
  const innerMethod = innerCall.callee.property.name;
12530
12195
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12597,10 +12262,10 @@ const jsFlatmapFilter = defineRule({
12597
12262
  if (!filterArgument) return;
12598
12263
  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;
12599
12264
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12600
- const innerCall = stripParenExpression(node.callee.object);
12265
+ const innerCall = node.callee.object;
12601
12266
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12602
12267
  if (innerCall.callee.property.name !== "map") return;
12603
- const receiver = stripParenExpression(innerCall.callee.object);
12268
+ const receiver = innerCall.callee.object;
12604
12269
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12605
12270
  const elements = receiver.elements ?? [];
12606
12271
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13860,7 +13525,7 @@ const jsTosortedImmutable = defineRule({
13860
13525
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13861
13526
  create: (context) => ({ CallExpression(node) {
13862
13527
  if (!isMemberProperty(node.callee, "sort")) return;
13863
- const receiver = stripParenExpression(node.callee.object);
13528
+ const receiver = node.callee.object;
13864
13529
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13865
13530
  const spreadArgument = receiver.elements[0].argument;
13866
13531
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18897,8 +18562,7 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18897
18562
  };
18898
18563
  const describeClientSideNavigation = (node) => {
18899
18564
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18900
- const receiver = stripParenExpression(node.callee.object);
18901
- const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18565
+ const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18902
18566
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18903
18567
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18904
18568
  }
@@ -19518,7 +19182,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19518
19182
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19519
19183
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19520
19184
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19521
- if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19185
+ if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19522
19186
  return node.callee.property.name;
19523
19187
  };
19524
19188
  const isMutatingFetchCall = (node) => {
@@ -19532,7 +19196,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19532
19196
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19533
19197
  const { property, object } = node.callee;
19534
19198
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19535
- if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19199
+ if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19536
19200
  return true;
19537
19201
  };
19538
19202
  const getDbCallDescription = (node) => {
@@ -19540,8 +19204,7 @@ const getDbCallDescription = (node) => {
19540
19204
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19541
19205
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19542
19206
  const methodName = node.callee.property.name;
19543
- const receiver = stripParenExpression(node.callee.object);
19544
- const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19207
+ const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19545
19208
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19546
19209
  };
19547
19210
  const findSideEffect = (node, options = {}) => {
@@ -20941,13 +20604,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20941
20604
  if (isNodeOfType(current, "CallExpression")) {
20942
20605
  let node = ref.identifier;
20943
20606
  let parent = node.parent;
20944
- while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20607
+ while (parent && isNodeOfType(parent, "MemberExpression")) {
20945
20608
  node = parent;
20946
20609
  parent = node.parent;
20947
20610
  }
20948
20611
  if (current.callee === node) return current;
20949
20612
  }
20950
- if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20613
+ if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20951
20614
  return null;
20952
20615
  };
20953
20616
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -21082,7 +20745,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
21082
20745
  const importDeclaration = declarationNode.parent;
21083
20746
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
21084
20747
  }));
21085
- const isHookCallee$1 = (analysis, node, hookName) => {
20748
+ const isHookCallee = (analysis, node, hookName) => {
21086
20749
  if (!node) return false;
21087
20750
  if (isNodeOfType(node, "Identifier")) {
21088
20751
  if (node.name === hookName) return true;
@@ -21091,19 +20754,15 @@ const isHookCallee$1 = (analysis, node, hookName) => {
21091
20754
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
21092
20755
  return false;
21093
20756
  }
21094
- if (isNodeOfType(node, "MemberExpression")) {
21095
- const receiver = stripParenExpression(node.object);
21096
- return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21097
- }
20757
+ if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21098
20758
  return false;
21099
20759
  };
21100
20760
  const isUseEffect = (node) => {
21101
20761
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
21102
20762
  const callee = node.callee;
21103
20763
  if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
21104
- if (!isNodeOfType(callee, "MemberExpression")) return false;
21105
- const receiver = stripParenExpression(callee.object);
21106
- return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
20764
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
20765
+ return false;
21107
20766
  };
21108
20767
  const getEffectFn = (analysis, node) => {
21109
20768
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -21131,7 +20790,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
21131
20790
  const node = def.node;
21132
20791
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
21133
20792
  if (!isNodeOfType(node.init, "CallExpression")) return false;
21134
- if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20793
+ if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21135
20794
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
21136
20795
  const elements = node.id.elements ?? [];
21137
20796
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -21142,7 +20801,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
21142
20801
  const node = def.node;
21143
20802
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
21144
20803
  if (!isNodeOfType(node.init, "CallExpression")) return false;
21145
- if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
20804
+ if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21146
20805
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
21147
20806
  const elements = node.id.elements ?? [];
21148
20807
  if (elements.length !== 2) return false;
@@ -21189,7 +20848,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
21189
20848
  const node = def.node;
21190
20849
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
21191
20850
  if (!isNodeOfType(node.init, "CallExpression")) return false;
21192
- return isHookCallee$1(analysis, node.init.callee, "useRef");
20851
+ return isHookCallee(analysis, node.init.callee, "useRef");
21193
20852
  }));
21194
20853
  const isRefCurrent = (ref) => {
21195
20854
  const parent = ref.identifier.parent;
@@ -21202,15 +20861,11 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
21202
20861
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
21203
20862
  const isPropCallbackInvocationRef = (analysis, ref) => {
21204
20863
  if (!isPropAlias(analysis, ref)) return false;
21205
- let effectiveNode = ref.identifier;
21206
- let parent = effectiveNode.parent;
21207
- while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21208
- effectiveNode = parent;
21209
- parent = effectiveNode.parent;
21210
- }
20864
+ const identifier = ref.identifier;
20865
+ const parent = identifier.parent;
21211
20866
  if (!parent) return false;
21212
- if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21213
- if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
20867
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
20868
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21214
20869
  const memberParent = parent.parent;
21215
20870
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
21216
20871
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -21221,7 +20876,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
21221
20876
  };
21222
20877
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
21223
20878
  const getUseStateDecl = (analysis, ref) => {
21224
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
20879
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21225
20880
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
21226
20881
  return node ?? null;
21227
20882
  };
@@ -21426,7 +21081,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21426
21081
  const noAdjustStateOnPropChange = defineRule({
21427
21082
  id: "no-adjust-state-on-prop-change",
21428
21083
  title: "State synced to a prop inside an effect",
21429
- severity: "warn",
21084
+ severity: "error",
21430
21085
  tags: ["test-noise"],
21431
21086
  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",
21432
21087
  create: (context) => ({ CallExpression(node) {
@@ -21467,23 +21122,7 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21467
21122
  "summary",
21468
21123
  "textarea"
21469
21124
  ]);
21470
- const isStaticallyFalseBooleanAttribute = (attribute) => {
21471
- const value = attribute.value;
21472
- if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21473
- const expression = value.expression;
21474
- return isNodeOfType(expression, "Literal") && expression.value === false;
21475
- };
21476
- const DISABLEABLE_TAGS = new Set([
21477
- "button",
21478
- "input",
21479
- "select",
21480
- "textarea"
21481
- ]);
21482
21125
  const isNativelyFocusable = (tagName, openingElement) => {
21483
- if (DISABLEABLE_TAGS.has(tagName)) {
21484
- const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21485
- if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21486
- }
21487
21126
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21488
21127
  switch (tagName) {
21489
21128
  case "input": {
@@ -21497,10 +21136,7 @@ const isNativelyFocusable = (tagName, openingElement) => {
21497
21136
  case "a":
21498
21137
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21499
21138
  case "audio":
21500
- case "video": {
21501
- const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21502
- return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21503
- }
21139
+ case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21504
21140
  default: return false;
21505
21141
  }
21506
21142
  };
@@ -22464,8 +22100,7 @@ const SECOND_INDEX_METHODS = new Set([
22464
22100
  "some"
22465
22101
  ]);
22466
22102
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22467
- const isPositionallyStableIterationReceiver = (receiverNode) => {
22468
- const receiver = stripParenExpression(receiverNode);
22103
+ const isPositionallyStableIterationReceiver = (receiver) => {
22469
22104
  if (isAllLiteralArrayExpression(receiver)) return true;
22470
22105
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22471
22106
  const only = receiver.elements[0];
@@ -22476,13 +22111,17 @@ const isPositionallyStableIterationReceiver = (receiverNode) => {
22476
22111
  }
22477
22112
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22478
22113
  const callee = receiver.callee;
22479
- if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22114
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22480
22115
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22481
22116
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22482
22117
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22483
22118
  return false;
22484
22119
  };
22485
- const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22120
+ const isArrayFromMapperCallback = (parentCall, callback) => {
22121
+ if (parentCall.arguments[1] !== callback) return false;
22122
+ const callee = parentCall.callee;
22123
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
22124
+ };
22486
22125
  const isArrayFromSourcePositionallyStable = (source) => {
22487
22126
  if (isNodeOfType(source, "ObjectExpression")) {
22488
22127
  for (const property of source.properties ?? []) {
@@ -22541,12 +22180,18 @@ const expressionUsesIndex = (expression, paramName) => {
22541
22180
  return false;
22542
22181
  }
22543
22182
  if (isNodeOfType(expression, "CallExpression")) {
22544
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22183
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
22545
22184
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22546
22185
  }
22547
22186
  return false;
22548
22187
  };
22549
- const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22188
+ const isReactCloneElement = (callExpression) => {
22189
+ const callee = callExpression.callee;
22190
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
22191
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
22192
+ if (callee.property.name !== "cloneElement") return false;
22193
+ return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
22194
+ };
22550
22195
  const noArrayIndexKey = defineRule({
22551
22196
  id: "no-array-index-key",
22552
22197
  title: "Array index used as a key",
@@ -23270,7 +22915,7 @@ const isAsyncFunctionLike = (node) => {
23270
22915
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
23271
22916
  return false;
23272
22917
  };
23273
- const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
22918
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23274
22919
  "forEach",
23275
22920
  "map",
23276
22921
  "filter",
@@ -23291,51 +22936,30 @@ const runsOnEffectDispatch = (functionNode) => {
23291
22936
  if (parent.callee === functionNode) return true;
23292
22937
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
23293
22938
  const callee = parent.callee;
23294
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
22939
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23295
22940
  };
23296
22941
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
23297
- const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23298
- const analyzeStatementSequence = (statements, context) => {
22942
+ const isGuardWithTerminatingBranch = (statement) => {
22943
+ if (!isNodeOfType(statement, "IfStatement")) return null;
22944
+ if (statement.alternate) return null;
22945
+ const consequent = statement.consequent;
22946
+ if (isTerminatingStatement(consequent)) return consequent;
22947
+ if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
22948
+ return null;
22949
+ };
22950
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23299
22951
  let fallThroughCount = 0;
23300
- let maxTerminatedCount = 0;
22952
+ let maxTerminatingPathCount = 0;
23301
22953
  for (const statement of statements) {
23302
- if (isNodeOfType(statement, "IfStatement")) {
23303
- fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23304
- const thenSummary = analyzeBranchStatements(statement.consequent, context);
23305
- const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23306
- fallThroughCount: 0,
23307
- maxTerminatedCount: 0,
23308
- doAllPathsTerminate: false
23309
- };
23310
- maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23311
- if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23312
- fallThroughCount: 0,
23313
- maxTerminatedCount,
23314
- doAllPathsTerminate: true
23315
- };
23316
- const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23317
- fallThroughCount += Math.max(...fallThroughBranchCounts);
22954
+ const guardBranch = isGuardWithTerminatingBranch(statement);
22955
+ if (guardBranch) {
22956
+ maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23318
22957
  continue;
23319
22958
  }
23320
- if (isTerminatingStatement(statement)) {
23321
- const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23322
- return {
23323
- fallThroughCount: 0,
23324
- maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23325
- doAllPathsTerminate: true
23326
- };
23327
- }
22959
+ if (isTerminatingStatement(statement)) break;
23328
22960
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
23329
22961
  }
23330
- return {
23331
- fallThroughCount,
23332
- maxTerminatedCount,
23333
- doAllPathsTerminate: false
23334
- };
23335
- };
23336
- const countStatementSequenceSetStateCalls = (statements, context) => {
23337
- const summary = analyzeStatementSequence(statements, context);
23338
- return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
22962
+ return Math.max(maxTerminatingPathCount, fallThroughCount);
23339
22963
  };
23340
22964
  const collectLocalHelperFunctions = (root) => {
23341
22965
  const helpersByName = /* @__PURE__ */ new Map();
@@ -23466,9 +23090,7 @@ const isDevOnlyGuardedEffect = (callback) => {
23466
23090
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23467
23091
  const firstStatement = (body.body ?? [])[0];
23468
23092
  if (!firstStatement) return false;
23469
- if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23470
- const consequent = firstStatement.consequent;
23471
- if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23093
+ if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23472
23094
  return mentionsDevEnvFlag(firstStatement.test);
23473
23095
  };
23474
23096
  const noCascadingSetState = defineRule({
@@ -23827,6 +23449,40 @@ const noCloneElement = defineRule({
23827
23449
  } })
23828
23450
  });
23829
23451
  //#endregion
23452
+ //#region src/plugin/utils/component-or-hook-display-name.ts
23453
+ const hocWrapperCalleeName = (callee) => {
23454
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
23455
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23456
+ return null;
23457
+ };
23458
+ const displayNameFromFunctionBinding = (functionNode) => {
23459
+ let current = functionNode;
23460
+ for (;;) {
23461
+ const parent = current.parent;
23462
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23463
+ const calleeName = hocWrapperCalleeName(parent.callee);
23464
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23465
+ current = parent;
23466
+ continue;
23467
+ }
23468
+ }
23469
+ break;
23470
+ }
23471
+ const binding = current.parent;
23472
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23473
+ return null;
23474
+ };
23475
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
23476
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23477
+ return displayNameFromFunctionBinding(functionNode);
23478
+ };
23479
+ //#endregion
23480
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23481
+ const enclosingComponentOrHookName = (node) => {
23482
+ const functionNode = findEnclosingFunction(node);
23483
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23484
+ };
23485
+ //#endregion
23830
23486
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23831
23487
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23832
23488
  const CONTEXT_MODULES = [
@@ -24797,21 +24453,11 @@ const isInitialOnlySetterCall = (callExpr) => {
24797
24453
  return isInitialOnlyPropName(arg.name);
24798
24454
  };
24799
24455
  //#endregion
24800
- //#region src/plugin/utils/is-no-op-statement.ts
24801
- const isNoOpStatement = (statement) => {
24802
- if (isNodeOfType(statement, "EmptyStatement")) return true;
24803
- if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24804
- const expression = stripParenExpression(statement.expression);
24805
- if (isNodeOfType(expression, "Literal")) return true;
24806
- if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24807
- if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24808
- return false;
24809
- };
24810
- //#endregion
24811
24456
  //#region src/plugin/utils/get-callback-statements.ts
24812
24457
  const getCallbackStatements = (callback) => {
24813
24458
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
24814
- return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24459
+ if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
24460
+ return callback.body ? [callback.body] : [];
24815
24461
  };
24816
24462
  //#endregion
24817
24463
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24850,27 +24496,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24850
24496
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24851
24497
  }
24852
24498
  };
24853
- const flattenGuardedStatements = (statements) => {
24854
- const flattened = [];
24855
- for (const statement of statements) {
24856
- if (isNoOpStatement(statement)) continue;
24857
- if (isNodeOfType(statement, "ExpressionStatement")) {
24858
- flattened.push(statement);
24859
- continue;
24860
- }
24861
- if (isNodeOfType(statement, "IfStatement")) {
24862
- for (const branch of [statement.consequent, statement.alternate]) {
24863
- if (!branch) continue;
24864
- const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24865
- if (flattenedBranch === null) return null;
24866
- flattened.push(...flattenedBranch);
24867
- }
24868
- continue;
24869
- }
24870
- return null;
24871
- }
24872
- return flattened;
24873
- };
24874
24499
  const noDerivedStateEffect = defineRule({
24875
24500
  id: "no-derived-state-effect",
24876
24501
  title: "Derived state stored in an effect",
@@ -24902,8 +24527,8 @@ const noDerivedStateEffect = defineRule({
24902
24527
  }
24903
24528
  }
24904
24529
  if (sawAnyDep && allDepsAreInitialOnly) return;
24905
- const statements = flattenGuardedStatements(getCallbackStatements(callback));
24906
- if (statements === null || statements.length === 0) return;
24530
+ const statements = getCallbackStatements(callback);
24531
+ if (statements.length === 0) return;
24907
24532
  if (!statements.every((statement) => {
24908
24533
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24909
24534
  const expression = statement.expression;
@@ -25538,7 +25163,7 @@ const noDidMountSetState = defineRule({
25538
25163
  const { mode } = resolveSettings$20(context.settings);
25539
25164
  return { CallExpression(node) {
25540
25165
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25541
- if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25166
+ if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25542
25167
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25543
25168
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25544
25169
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25663,7 +25288,7 @@ const noDidUpdateSetState = defineRule({
25663
25288
  const { mode } = resolveSettings$19(context.settings);
25664
25289
  return { CallExpression(node) {
25665
25290
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25666
- if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25291
+ if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25667
25292
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25668
25293
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25669
25294
  if (isInsideDiffGuard(node)) return;
@@ -25760,45 +25385,18 @@ const collectUseStateBindings = (componentBody) => {
25760
25385
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25761
25386
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25762
25387
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
25763
- const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
25764
- const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
25765
- "assign",
25766
- "entries",
25767
- "fromEntries",
25768
- "keys",
25769
- "values"
25770
- ]);
25771
- const ARRAY_COPY_METHOD_NAMES = new Set([
25772
- "map",
25773
- "filter",
25774
- "slice",
25775
- "concat",
25776
- "flat",
25777
- "flatMap",
25778
- "toSorted",
25779
- "toReversed",
25780
- "toSpliced",
25781
- "with"
25782
- ]);
25783
25388
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25784
25389
  const isPlainDataProducerCall = (expression) => {
25785
25390
  if (!isNodeOfType(expression, "CallExpression")) return false;
25786
25391
  const callee = expression.callee;
25787
25392
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25788
25393
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
25789
- if (isNodeOfType(callee.object, "Identifier")) {
25790
- if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25791
- if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
25792
- if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
25793
- }
25394
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25794
25395
  return producesPlainStateValue(callee.object);
25795
25396
  };
25796
- const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
25797
- const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25798
25397
  const producesPlainStateValue = (expression) => {
25799
25398
  const unwrapped = stripParenExpression(expression);
25800
25399
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
25801
- if (isPlainDataNewExpression(unwrapped)) return true;
25802
25400
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25803
25401
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25804
25402
  return isPlainDataProducerCall(unwrapped);
@@ -25813,38 +25411,6 @@ const initializerMarksPlainState = (initializerArgument) => {
25813
25411
  }
25814
25412
  return producesPlainStateValue(unwrapped);
25815
25413
  };
25816
- const producesOpaqueInstanceValue = (expression) => {
25817
- if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
25818
- if (!isNodeOfType(expression, "CallExpression")) return false;
25819
- const callee = expression.callee;
25820
- if (!isNodeOfType(callee, "MemberExpression")) return false;
25821
- if (isPlainDataProducerCall(expression)) return false;
25822
- if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
25823
- return true;
25824
- };
25825
- const collectSetterValueObservations = (componentBody, setterNames) => {
25826
- const plainFedSetterNames = /* @__PURE__ */ new Set();
25827
- const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25828
- walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
25829
- if (!isNodeOfType(node, "CallExpression")) return;
25830
- if (!isNodeOfType(node.callee, "Identifier")) return;
25831
- const setterName = node.callee.name;
25832
- if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
25833
- const argument = node.arguments?.[0];
25834
- if (!argument) return;
25835
- const unwrapped = stripParenExpression(argument);
25836
- if (isNullOrUndefinedExpression(unwrapped)) return;
25837
- if (producesPlainStateValue(unwrapped)) {
25838
- plainFedSetterNames.add(setterName);
25839
- return;
25840
- }
25841
- if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25842
- }, true);
25843
- return {
25844
- plainFedSetterNames,
25845
- opaqueFedSetterNames
25846
- };
25847
- };
25848
25414
  const collectCallbackRefSetterNames = (componentBody) => {
25849
25415
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25850
25416
  walkAst(componentBody, (node) => {
@@ -25914,15 +25480,11 @@ const noDirectStateMutation = defineRule({
25914
25480
  if (bindings.length === 0) return;
25915
25481
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25916
25482
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
25917
- const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25918
25483
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25919
25484
  for (const binding of bindings) {
25920
25485
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25921
25486
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25922
- const initializerArgument = binding.declarator.init.arguments?.[0];
25923
- if (!initializerMarksPlainState(initializerArgument)) continue;
25924
- if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
25925
- plainObjectStateValueNames.add(binding.valueName);
25487
+ if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
25926
25488
  }
25927
25489
  const visitMutationCandidate = (child, currentlyShadowed) => {
25928
25490
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -26047,10 +25609,9 @@ const noDocumentStartViewTransition = defineRule({
26047
25609
  create: (context) => ({ CallExpression(node) {
26048
25610
  const callee = node.callee;
26049
25611
  if (!isNodeOfType(callee, "MemberExpression")) return;
26050
- const receiver = stripParenExpression(callee.object);
26051
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25612
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26052
25613
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
26053
- if (context.scopes.symbolFor(receiver) !== null) return;
25614
+ if (context.scopes.symbolFor(callee.object) !== null) return;
26054
25615
  if (!importsReactViewTransition(node)) return;
26055
25616
  context.report({
26056
25617
  node,
@@ -26070,8 +25631,7 @@ const noDocumentWrite = defineRule({
26070
25631
  create: (context) => ({ CallExpression(node) {
26071
25632
  const callee = node.callee;
26072
25633
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
26073
- const receiver = stripParenExpression(callee.object);
26074
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25634
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
26075
25635
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
26076
25636
  context.report({
26077
25637
  node,
@@ -26703,6 +26263,13 @@ const noEffectEventHandler = defineRule({
26703
26263
  }
26704
26264
  });
26705
26265
  //#endregion
26266
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
26267
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
26268
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
26269
+ if (importSource === null) return false;
26270
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
26271
+ };
26272
+ //#endregion
26706
26273
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26707
26274
  const createComponentBindingStackTracker = (callbacks) => {
26708
26275
  const componentBindingStack = [];
@@ -26756,7 +26323,11 @@ const noEffectEventInDeps = defineRule({
26756
26323
  const initializer = declaratorNode.init;
26757
26324
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26758
26325
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26759
- if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26326
+ if (isNodeOfType(initializer.callee, "Identifier")) {
26327
+ if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26328
+ const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26329
+ if (calleeSymbol && calleeSymbol.kind !== "import") return;
26330
+ }
26760
26331
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26761
26332
  } });
26762
26333
  return {
@@ -29336,7 +28907,7 @@ const noIsMounted = defineRule({
29336
28907
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
29337
28908
  create: (context) => ({ CallExpression(node) {
29338
28909
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
29339
- if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
28910
+ if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29340
28911
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
29341
28912
  if (!getParentComponent(node)) return;
29342
28913
  context.report({
@@ -29351,9 +28922,7 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
29351
28922
  const isJsonMethodCall = (node, method) => {
29352
28923
  if (!isNodeOfType(node, "CallExpression")) return false;
29353
28924
  const callee = node.callee;
29354
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29355
- const receiver = stripParenExpression(callee.object);
29356
- return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
28925
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
29357
28926
  };
29358
28927
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
29359
28928
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -29438,7 +29007,7 @@ const extractReturnTypeAnnotation = (returnType) => {
29438
29007
  const noJsxElementType = defineRule({
29439
29008
  id: "no-jsx-element-type",
29440
29009
  title: "No JSX.Element",
29441
- severity: "warn",
29010
+ severity: "error",
29442
29011
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
29443
29012
  create: (context) => {
29444
29013
  let isJsxImported = false;
@@ -29764,378 +29333,6 @@ const noLegacyContextApi = defineRule({
29764
29333
  }
29765
29334
  });
29766
29335
  //#endregion
29767
- //#region src/plugin/utils/executes-during-render.ts
29768
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29769
- "map",
29770
- "filter",
29771
- "forEach",
29772
- "flatMap",
29773
- "reduce",
29774
- "reduceRight",
29775
- "some",
29776
- "every",
29777
- "find",
29778
- "findIndex",
29779
- "findLast",
29780
- "findLastIndex",
29781
- "sort",
29782
- "toSorted"
29783
- ]);
29784
- const executesDuringRender = (functionNode) => {
29785
- const parent = functionNode.parent;
29786
- if (!isNodeOfType(parent, "CallExpression")) return false;
29787
- if (parent.callee === functionNode) return true;
29788
- if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29789
- 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;
29790
- };
29791
- //#endregion
29792
- //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29793
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
29794
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29795
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29796
- return false;
29797
- };
29798
- //#endregion
29799
- //#region src/plugin/utils/find-declarator-for-binding.ts
29800
- const findDeclaratorForBinding = (bindingIdentifier) => {
29801
- let ancestor = bindingIdentifier.parent;
29802
- while (ancestor) {
29803
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29804
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29805
- ancestor = ancestor.parent ?? null;
29806
- }
29807
- return null;
29808
- };
29809
- //#endregion
29810
- //#region src/plugin/utils/references-falsy-initial-state.ts
29811
- const isFalsyLiteral = (node) => {
29812
- if (!node) return true;
29813
- if (isNodeOfType(node, "Literal")) return !node.value;
29814
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
29815
- };
29816
- const isFalsyInitialStateBinding = (identifier) => {
29817
- if (!isNodeOfType(identifier, "Identifier")) return false;
29818
- const binding = findVariableInitializer(identifier, identifier.name);
29819
- if (!binding) return false;
29820
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29821
- if (!declarator?.init) return false;
29822
- const init = stripParenExpression(declarator.init);
29823
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29824
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29825
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29826
- return isFalsyLiteral(init.arguments?.[0]);
29827
- };
29828
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29829
- //#endregion
29830
- //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29831
- const isGatedByFalsyInitialState = (node) => {
29832
- let cursor = node;
29833
- let parent = node.parent;
29834
- while (parent) {
29835
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29836
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29837
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29838
- cursor = parent;
29839
- parent = parent.parent ?? null;
29840
- }
29841
- return false;
29842
- };
29843
- //#endregion
29844
- //#region src/plugin/utils/references-client-only-flag.ts
29845
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29846
- const referencesClientOnlyFlag = (expression) => {
29847
- const unwrapped = stripParenExpression(expression);
29848
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29849
- if (isNodeOfType(unwrapped, "MemberExpression")) {
29850
- const property = unwrapped.property;
29851
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29852
- }
29853
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29854
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29855
- return false;
29856
- };
29857
- //#endregion
29858
- //#region src/plugin/utils/is-inside-client-only-guard.ts
29859
- const isInsideClientOnlyGuard = (node) => {
29860
- let cursor = node;
29861
- let parent = node.parent;
29862
- while (parent) {
29863
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29864
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29865
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29866
- cursor = parent;
29867
- parent = parent.parent ?? null;
29868
- }
29869
- return false;
29870
- };
29871
- //#endregion
29872
- //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29873
- const LOCALE_FORMAT_METHOD_NAMES = new Set([
29874
- "toLocaleString",
29875
- "toLocaleDateString",
29876
- "toLocaleTimeString"
29877
- ]);
29878
- const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29879
- const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29880
- const INTL_FORMAT_METHOD_NAMES = new Set([
29881
- "format",
29882
- "formatToParts",
29883
- "formatRange"
29884
- ]);
29885
- const isProvableDateExpression = (expression) => {
29886
- if (!expression) return false;
29887
- const unwrapped = stripParenExpression(expression);
29888
- return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29889
- };
29890
- const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29891
- const receiverNameLooksDateFlavored = (expression) => {
29892
- if (!expression) return false;
29893
- const unwrapped = stripParenExpression(expression);
29894
- if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29895
- if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29896
- if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29897
- return false;
29898
- };
29899
- const objectLiteralHasProperty = (objectExpression, propertyName) => {
29900
- if (!objectExpression) return false;
29901
- const unwrapped = stripParenExpression(objectExpression);
29902
- if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29903
- for (const property of unwrapped.properties ?? []) {
29904
- if (!isNodeOfType(property, "Property")) continue;
29905
- if (property.computed) continue;
29906
- if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29907
- if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29908
- }
29909
- return false;
29910
- };
29911
- const hasExplicitLocaleArgument = (argument) => {
29912
- if (!argument) return false;
29913
- const unwrapped = stripParenExpression(argument);
29914
- if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29915
- return true;
29916
- };
29917
- const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29918
- const localeArgument = call.arguments?.[0];
29919
- if (!hasExplicitLocaleArgument(localeArgument)) return false;
29920
- const optionsArgument = call.arguments?.[1];
29921
- if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29922
- return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29923
- };
29924
- const matchLocaleMethodCall = (call) => {
29925
- const callee = call.callee;
29926
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29927
- if (!isNodeOfType(callee.property, "Identifier")) return null;
29928
- const methodName = callee.property.name;
29929
- if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29930
- const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29931
- if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29932
- if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29933
- return {
29934
- node: call,
29935
- display: `${methodName}()`
29936
- };
29937
- };
29938
- const getIntlFormatterName = (expression) => {
29939
- if (!expression) return null;
29940
- const unwrapped = stripParenExpression(expression);
29941
- if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29942
- const callee = unwrapped.callee;
29943
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29944
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29945
- if (!isNodeOfType(callee.property, "Identifier")) return null;
29946
- return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29947
- };
29948
- const isDeterministicIntlConstruction = (construction, formatterName) => {
29949
- if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29950
- if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29951
- if (formatterName !== "DateTimeFormat") return true;
29952
- return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29953
- };
29954
- const matchIntlFormatCall = (call) => {
29955
- const callee = call.callee;
29956
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29957
- if (!isNodeOfType(callee.property, "Identifier")) return null;
29958
- if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29959
- let construction = stripParenExpression(callee.object);
29960
- if (isNodeOfType(construction, "Identifier")) {
29961
- const binding = findVariableInitializer(construction, construction.name);
29962
- construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29963
- }
29964
- if (!construction) return null;
29965
- const formatterName = getIntlFormatterName(construction);
29966
- if (!formatterName) return null;
29967
- if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29968
- return {
29969
- node: call,
29970
- display: `Intl.${formatterName}().${callee.property.name}()`
29971
- };
29972
- };
29973
- const isDeterministicInputDateConstruction = (expression) => {
29974
- if (!expression) return false;
29975
- const unwrapped = stripParenExpression(expression);
29976
- if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29977
- if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29978
- return (unwrapped.arguments?.length ?? 0) > 0;
29979
- };
29980
- const matchDateDefaultStringification = (node) => {
29981
- if (isNodeOfType(node, "CallExpression")) {
29982
- const callee = node.callee;
29983
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29984
- node,
29985
- display: "Date.prototype.toString()"
29986
- };
29987
- if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29988
- node,
29989
- display: "String(new Date(…))"
29990
- };
29991
- return null;
29992
- }
29993
- if (isNodeOfType(node, "TemplateLiteral")) {
29994
- for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29995
- node: expression,
29996
- display: "`${new Date(…)}`"
29997
- };
29998
- }
29999
- return null;
30000
- };
30001
- const findRenderPhaseComponentOrHook = (node) => {
30002
- let functionNode = findEnclosingFunction(node);
30003
- while (functionNode) {
30004
- if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30005
- if (!executesDuringRender(functionNode)) return null;
30006
- functionNode = findEnclosingFunction(functionNode);
30007
- }
30008
- return null;
30009
- };
30010
- const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30011
- if (fileHasUseClientDirective) return true;
30012
- const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30013
- if (displayName && isReactHookName(displayName)) return true;
30014
- let callsHook = false;
30015
- walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30016
- if (callsHook) return false;
30017
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30018
- callsHook = true;
30019
- return false;
30020
- }
30021
- });
30022
- return callsHook;
30023
- };
30024
- const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30025
- const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30026
- if (!isNodeOfType(body, "BlockStatement")) return false;
30027
- const ancestors = /* @__PURE__ */ new Set();
30028
- let cursor = node;
30029
- while (cursor) {
30030
- ancestors.add(cursor);
30031
- cursor = cursor.parent ?? null;
30032
- }
30033
- for (const statement of body.body ?? []) {
30034
- if (ancestors.has(statement)) return false;
30035
- if (!isNodeOfType(statement, "IfStatement")) continue;
30036
- if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30037
- let returnsEarly = false;
30038
- walkAst(statement.consequent, (child) => {
30039
- if (isFunctionLike$1(child)) return false;
30040
- if (isNodeOfType(child, "ReturnStatement")) {
30041
- returnsEarly = true;
30042
- return false;
30043
- }
30044
- });
30045
- if (returnsEarly) return true;
30046
- }
30047
- return false;
30048
- };
30049
- const findEnclosingJsxOpeningElement = (node) => {
30050
- let cursor = node.parent;
30051
- while (cursor) {
30052
- if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30053
- if (isNodeOfType(cursor, "JSXFragment")) return null;
30054
- cursor = cursor.parent ?? null;
30055
- }
30056
- return null;
30057
- };
30058
- const noLocaleFormatInRender = defineRule({
30059
- id: "no-locale-format-in-render",
30060
- title: "Locale/timezone formatting during render",
30061
- severity: "warn",
30062
- category: "Correctness",
30063
- disabledWhen: [
30064
- "vite",
30065
- "cra",
30066
- "expo",
30067
- "react-native",
30068
- "unknown"
30069
- ],
30070
- 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.",
30071
- create: (context) => {
30072
- if (isTestlikeFilename(context.filename)) return {};
30073
- if (classifyReactNativeFileTarget(context) === "react-native") return {};
30074
- let fileHasUseClientDirective = false;
30075
- let fileIsEmailTemplate = false;
30076
- const reportedNodes = /* @__PURE__ */ new Set();
30077
- const reportIfRenderPhase = (match) => {
30078
- if (reportedNodes.has(match.node)) return;
30079
- const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30080
- if (!componentOrHookNode) return;
30081
- if (fileIsEmailTemplate) return;
30082
- if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30083
- if (isInsideClientOnlyGuard(match.node)) return;
30084
- if (isGatedByFalsyInitialState(match.node)) return;
30085
- if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30086
- if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30087
- if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30088
- reportedNodes.add(match.node);
30089
- context.report({
30090
- node: match.node,
30091
- 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.`
30092
- });
30093
- };
30094
- return {
30095
- Program(node) {
30096
- fileHasUseClientDirective = hasDirective(node, "use client");
30097
- fileIsEmailTemplate = hasEmailTemplateImport(node);
30098
- },
30099
- CallExpression(node) {
30100
- const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30101
- if (match) reportIfRenderPhase(match);
30102
- },
30103
- TemplateLiteral(node) {
30104
- const match = matchDateDefaultStringification(node);
30105
- if (match) reportIfRenderPhase(match);
30106
- },
30107
- JSXExpressionContainer(node) {
30108
- const expression = stripParenExpression(node.expression);
30109
- if (!isNodeOfType(expression, "CallExpression")) return;
30110
- if (!isNodeOfType(expression.callee, "Identifier")) return;
30111
- const helperName = expression.callee.name;
30112
- const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30113
- if (!componentOrHookNode) return;
30114
- const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30115
- if (!helperNode || !isFunctionLike$1(helperNode)) return;
30116
- if (componentOrHookDisplayNameForFunction(helperNode)) return;
30117
- walkAst(helperNode.body ?? helperNode, (child) => {
30118
- if (isFunctionLike$1(child)) return false;
30119
- if (!isNodeOfType(child, "CallExpression")) return;
30120
- const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30121
- if (!match || reportedNodes.has(match.node)) return;
30122
- if (fileIsEmailTemplate) return;
30123
- if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30124
- if (isInsideClientOnlyGuard(node)) return;
30125
- if (isGatedByFalsyInitialState(node)) return;
30126
- if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30127
- if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30128
- reportedNodes.add(match.node);
30129
- context.report({
30130
- node: match.node,
30131
- 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.`
30132
- });
30133
- });
30134
- }
30135
- };
30136
- }
30137
- });
30138
- //#endregion
30139
29336
  //#region src/plugin/rules/design/no-long-transition-duration.ts
30140
29337
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
30141
29338
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -30949,6 +30146,7 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30949
30146
  "reverse",
30950
30147
  "sort"
30951
30148
  ]);
30149
+ const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30952
30150
  const OBJECT_MUTATION_METHODS = new Set([
30953
30151
  "assign",
30954
30152
  "defineProperties",
@@ -31022,6 +30220,7 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
31022
30220
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
31023
30221
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
31024
30222
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30223
+ if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
31025
30224
  }
31026
30225
  }
31027
30226
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -31060,7 +30259,6 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
31060
30259
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
31061
30260
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
31062
30261
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
31063
- if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
31064
30262
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
31065
30263
  });
31066
30264
  return mutations;
@@ -31293,7 +30491,7 @@ const noNestedComponentDefinition = defineRule({
31293
30491
  id: "no-nested-component-definition",
31294
30492
  title: "Component defined inside another component",
31295
30493
  tags: ["test-noise", "react-jsx-only"],
31296
- severity: "warn",
30494
+ severity: "error",
31297
30495
  category: "Correctness",
31298
30496
  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.",
31299
30497
  create: (context) => {
@@ -32248,12 +31446,12 @@ const noPassDataToParent = defineRule({
32248
31446
  if (calleeNode === identifier) {
32249
31447
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
32250
31448
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
32251
- } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31449
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32252
31450
  if (!isWholePropsObjectReference(analysis, ref)) continue;
32253
31451
  if (isCustomHookParameter(ref)) continue;
32254
31452
  } else continue;
32255
31453
  const methodName = getCallMethodName(calleeNode);
32256
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31454
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32257
31455
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
32258
31456
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
32259
31457
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -32444,7 +31642,7 @@ const noPassLiveStateToParent = defineRule({
32444
31642
  if (isCallResultConsumedAsArgument(callExpr)) continue;
32445
31643
  const calleeNode = callExpr.callee;
32446
31644
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
32447
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31645
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32448
31646
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
32449
31647
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
32450
31648
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -32772,6 +31970,40 @@ const noPreventDefault = defineRule({
32772
31970
  }
32773
31971
  });
32774
31972
  //#endregion
31973
+ //#region src/plugin/utils/is-result-discarded-call.ts
31974
+ const isResultDiscardedCall = (callExpression) => {
31975
+ let node = callExpression;
31976
+ let parent = node.parent;
31977
+ while (parent) {
31978
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
31979
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31980
+ if (isNodeOfType(parent, "ChainExpression")) {
31981
+ node = parent;
31982
+ parent = node.parent;
31983
+ continue;
31984
+ }
31985
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31986
+ node = parent;
31987
+ parent = node.parent;
31988
+ continue;
31989
+ }
31990
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31991
+ node = parent;
31992
+ parent = node.parent;
31993
+ continue;
31994
+ }
31995
+ if (isNodeOfType(parent, "SequenceExpression")) {
31996
+ const expressions = parent.expressions ?? [];
31997
+ if (expressions[expressions.length - 1] !== node) return true;
31998
+ node = parent;
31999
+ parent = node.parent;
32000
+ continue;
32001
+ }
32002
+ return false;
32003
+ }
32004
+ return false;
32005
+ };
32006
+ //#endregion
32775
32007
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
32776
32008
  const isRefLatchGuardedEffect = (callbackBody) => {
32777
32009
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32978,7 +32210,7 @@ const isAlwaysFreshExpression = (expression) => {
32978
32210
  return `${callee.name}()`;
32979
32211
  }
32980
32212
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32981
- const receiver = stripParenExpression(callee.object);
32213
+ const receiver = callee.object;
32982
32214
  const property = callee.property;
32983
32215
  if (!isNodeOfType(property, "Identifier")) return null;
32984
32216
  if (isNodeOfType(receiver, "Identifier")) {
@@ -33099,10 +32331,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
33099
32331
  if (typeof sourceValue !== "string") return;
33100
32332
  if (handleExtraSource?.(node, context)) return;
33101
32333
  if (sourceValue !== source) return;
33102
- if (isTypeOnlyImport(node)) return;
33103
32334
  for (const specifier of node.specifiers ?? []) {
33104
32335
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33105
- if (specifier.importKind === "type") continue;
33106
32336
  const importedName = getImportedName$1(specifier);
33107
32337
  if (!importedName) continue;
33108
32338
  const message = messages.get(importedName);
@@ -33121,9 +32351,8 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
33121
32351
  MemberExpression(node) {
33122
32352
  if (namespaceBindings.size === 0) return;
33123
32353
  if (node.computed) return;
33124
- const receiver = stripParenExpression(node.object);
33125
- if (!isNodeOfType(receiver, "Identifier")) return;
33126
- if (!namespaceBindings.has(receiver.name)) return;
32354
+ if (!isNodeOfType(node.object, "Identifier")) return;
32355
+ if (!namespaceBindings.has(node.object.name)) return;
33127
32356
  if (!isNodeOfType(node.property, "Identifier")) return;
33128
32357
  const message = messages.get(node.property.name);
33129
32358
  if (message) context.report({
@@ -33188,7 +32417,7 @@ const noReactDomDeprecatedApis = defineRule({
33188
32417
  });
33189
32418
  //#endregion
33190
32419
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
33191
- 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."]]);
32420
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
33192
32421
  const isVendoredShadcnUiFilename = (rawFilename) => {
33193
32422
  if (!rawFilename) return false;
33194
32423
  const filename = rawFilename.replaceAll("\\", "/");
@@ -33226,7 +32455,7 @@ const noReact19DeprecatedApis = defineRule({
33226
32455
  requires: ["react:19"],
33227
32456
  tags: ["test-noise", "migration-hint"],
33228
32457
  severity: "warn",
33229
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32458
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
33230
32459
  create: (context) => {
33231
32460
  if (isVendoredShadcnUiFilename(context.filename)) return {};
33232
32461
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -33550,11 +32779,34 @@ const noRedundantShouldComponentUpdate = defineRule({
33550
32779
  });
33551
32780
  //#endregion
33552
32781
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32782
+ const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32783
+ if (!symbol || visitedSymbols.has(symbol)) return false;
32784
+ visitedSymbols.add(symbol);
32785
+ if (isComponentParameterSymbol(symbol)) return true;
32786
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32787
+ const source = symbol.initializer;
32788
+ if (!source) return false;
32789
+ return initializerRootsInProps(source, scopes, visitedSymbols);
32790
+ };
32791
+ const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32792
+ if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32793
+ if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32794
+ return rootsInProps(node, scopes, visitedSymbols);
32795
+ };
32796
+ const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32797
+ let current = node;
32798
+ while (isNodeOfType(current, "MemberExpression")) {
32799
+ if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32800
+ current = current.object;
32801
+ }
32802
+ if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32803
+ return false;
32804
+ };
33553
32805
  const isInsideComponentContext = (node) => {
33554
32806
  let cursor = node.parent;
33555
32807
  while (cursor) {
32808
+ if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
33556
32809
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33557
- if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
33558
32810
  cursor = cursor.parent ?? null;
33559
32811
  }
33560
32812
  return false;
@@ -33564,28 +32816,24 @@ const functionBodyOf = (node) => {
33564
32816
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
33565
32817
  return null;
33566
32818
  };
33567
- const isHookCallee = (callee) => {
33568
- if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33569
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33570
- return false;
33571
- };
33572
32819
  const containsHookCall = (body) => {
33573
32820
  let found = false;
33574
32821
  walkAst(body, (child) => {
33575
- if (found) return false;
33576
- if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32822
+ if (found) return;
33577
32823
  if (!isNodeOfType(child, "CallExpression")) return;
33578
- if (isHookCallee(child.callee)) found = true;
32824
+ const name = getCalleeName$2(child);
32825
+ if (name && isReactHookName(name)) found = true;
33579
32826
  });
33580
32827
  return found;
33581
32828
  };
33582
- const isHookCallingRenderHelper = (symbol) => {
32829
+ const isModuleScopeHookFreeHelper = (symbol) => {
33583
32830
  if (!symbol) return false;
33584
32831
  const declaration = symbol.declarationNode;
33585
32832
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
33586
32833
  const body = functionBodyOf(declaration);
33587
32834
  if (!body) return false;
33588
- return containsHookCall(body);
32835
+ if (findEnclosingFunction(declaration) !== null) return false;
32836
+ return !containsHookCall(body);
33589
32837
  };
33590
32838
  const noRenderInRender = defineRule({
33591
32839
  id: "no-render-in-render",
@@ -33594,13 +32842,20 @@ const noRenderInRender = defineRule({
33594
32842
  tags: ["test-noise"],
33595
32843
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
33596
32844
  create: (context) => ({ JSXExpressionContainer(node) {
33597
- const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
32845
+ const expression = node.expression;
33598
32846
  if (!isNodeOfType(expression, "CallExpression")) return;
33599
- if (!isNodeOfType(expression.callee, "Identifier")) return;
33600
- const calleeName = expression.callee.name;
33601
- if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32847
+ let calleeName = null;
32848
+ if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32849
+ else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32850
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33602
32851
  if (!isInsideComponentContext(node)) return;
33603
- if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32852
+ if (isNodeOfType(expression.callee, "Identifier")) {
32853
+ const calleeSymbol = context.scopes.symbolFor(expression.callee);
32854
+ if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32855
+ if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32856
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32857
+ if (rootsInProps(expression.callee.object, context.scopes)) return;
32858
+ }
33604
32859
  context.report({
33605
32860
  node: expression,
33606
32861
  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.`
@@ -33661,7 +32916,13 @@ const noRenderPropChildren = defineRule({
33661
32916
  //#endregion
33662
32917
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
33663
32918
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
33664
- const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
32919
+ const isReactDomRenderCall = (node) => {
32920
+ if (!isNodeOfType(node.callee, "MemberExpression")) return false;
32921
+ if (!isNodeOfType(node.callee.object, "Identifier")) return false;
32922
+ if (node.callee.object.name !== "ReactDOM") return false;
32923
+ if (!isNodeOfType(node.callee.property, "Identifier")) return false;
32924
+ return node.callee.property.name === "render";
32925
+ };
33665
32926
  const isUsedAsReturnValue = (parent) => {
33666
32927
  if (!parent) return false;
33667
32928
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -34497,7 +33758,7 @@ const noSetState = defineRule({
34497
33758
  category: "Architecture",
34498
33759
  create: (context) => ({ CallExpression(node) {
34499
33760
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
34500
- if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33761
+ if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34501
33762
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
34502
33763
  if (!getParentComponent(node)) return;
34503
33764
  context.report({
@@ -36943,36 +36204,6 @@ const isTriviallyCheapExpression = (node) => {
36943
36204
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36944
36205
  return true;
36945
36206
  };
36946
- const isTrivialContainerLiteral = (node) => {
36947
- if (!node) return false;
36948
- const innerExpression = stripParenExpression(node);
36949
- if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36950
- if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36951
- return false;
36952
- };
36953
- const isNonEscapingRead = (identifier) => {
36954
- const readRoot = findTransparentExpressionRoot(identifier);
36955
- const memberNode = readRoot.parent;
36956
- if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
36957
- const memberUse = findTransparentExpressionRoot(memberNode);
36958
- const memberUseParent = memberUse.parent;
36959
- if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
36960
- if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
36961
- if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
36962
- return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
36963
- };
36964
- const isMemoIdentityUnused = (memoCallNode, scopes) => {
36965
- const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
36966
- const parentNode = memoUsageRoot.parent;
36967
- if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36968
- if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
36969
- const bindingTarget = parentNode.id;
36970
- if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36971
- if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36972
- const symbol = scopes.symbolFor(bindingTarget);
36973
- if (!symbol) return false;
36974
- return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36975
- };
36976
36207
  const noUsememoSimpleExpression = defineRule({
36977
36208
  id: "no-usememo-simple-expression",
36978
36209
  title: "useMemo on a cheap value",
@@ -36994,17 +36225,9 @@ const noUsememoSimpleExpression = defineRule({
36994
36225
  let returnExpression = null;
36995
36226
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36996
36227
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36997
- if (!returnExpression) return;
36998
- if (isTriviallyCheapExpression(returnExpression)) {
36999
- context.report({
37000
- node,
37001
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37002
- });
37003
- return;
37004
- }
37005
- if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36228
+ if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
37006
36229
  node,
37007
- message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36230
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
37008
36231
  });
37009
36232
  } })
37010
36233
  });
@@ -37110,7 +36333,7 @@ const noWillUpdateSetState = defineRule({
37110
36333
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
37111
36334
  return { CallExpression(node) {
37112
36335
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
37113
- if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36336
+ if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37114
36337
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
37115
36338
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
37116
36339
  context.report({
@@ -37427,10 +36650,6 @@ const isRouteFactoryCall = (expression) => {
37427
36650
  }
37428
36651
  return false;
37429
36652
  };
37430
- const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
37431
- const expression = skipTsExpression(argument);
37432
- return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37433
- });
37434
36653
  const isReactHocName = (name, state) => state.customHocs.has(name);
37435
36654
  const isHocCallee = (callee, state) => {
37436
36655
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -37470,7 +36689,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
37470
36689
  continue;
37471
36690
  }
37472
36691
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
37473
- if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
36692
+ if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
37474
36693
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
37475
36694
  }
37476
36695
  return false;
@@ -37578,22 +36797,18 @@ const onlyExportComponents = defineRule({
37578
36797
  customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
37579
36798
  allowExportNames: new Set(settings.allowExportNames),
37580
36799
  allowConstantExport: settings.allowConstantExport,
37581
- localComponentNames,
37582
- scopes: context.scopes
36800
+ localComponentNames
37583
36801
  };
37584
36802
  for (const child of componentCandidates) {
37585
36803
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
37586
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
36804
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37587
36805
  }
37588
36806
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
37589
36807
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37590
36808
  }
37591
36809
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
37592
36810
  const initializer = child.init;
37593
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37594
- const expression = initializer ? skipTsExpression(initializer) : null;
37595
- if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37596
- }
36811
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37597
36812
  }
37598
36813
  }
37599
36814
  const exports = [];
@@ -37663,10 +36878,6 @@ const onlyExportComponents = defineRule({
37663
36878
  return false;
37664
36879
  })();
37665
36880
  if (isHoc && firstArgIsValid) hasReactExport = true;
37666
- else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
37667
- kind: "non-component",
37668
- reportNode: stripped
37669
- });
37670
36881
  else context.report({
37671
36882
  node: stripped,
37672
36883
  message: ANONYMOUS_MESSAGE
@@ -38557,8 +37768,8 @@ const preferHtmlDialog = defineRule({
38557
37768
  if (!HTML_TAGS.has(tagName)) return;
38558
37769
  const roleAttribute = findJsxAttribute(node.attributes, "role");
38559
37770
  if (roleAttribute) {
38560
- const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
38561
- if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37771
+ const roleValue = getJsxPropStringValue(roleAttribute);
37772
+ if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
38562
37773
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
38563
37774
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
38564
37775
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -39262,22 +38473,11 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
39262
38473
  }
39263
38474
  return null;
39264
38475
  };
39265
- const isTrivialLiteralExpression = (expression) => {
39266
- if (isNodeOfType(expression, "Literal")) return true;
39267
- if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39268
- if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39269
- if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39270
- return false;
39271
- };
39272
38476
  const getSingleSetterCallFromHandler = (handler) => {
39273
38477
  const handlerStatements = getCallbackStatements(handler);
39274
38478
  if (handlerStatements.length !== 1) return null;
39275
38479
  const onlyStatement = handlerStatements[0];
39276
- let expression = onlyStatement;
39277
- if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39278
- if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39279
- if (!expression) return null;
39280
- expression = stripParenExpression(expression);
38480
+ const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39281
38481
  if (!isNodeOfType(expression, "CallExpression")) return null;
39282
38482
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
39283
38483
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -39287,106 +38487,6 @@ const getSingleSetterCallFromHandler = (handler) => {
39287
38487
  setterArgument: expression.arguments[0]
39288
38488
  };
39289
38489
  };
39290
- const isListenerCollectionInitializer = (init) => {
39291
- if (!init) return false;
39292
- if (isNodeOfType(init, "ArrayExpression")) return true;
39293
- return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39294
- };
39295
- const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39296
- if (!isFunctionLike$1(functionNode)) return false;
39297
- const firstParam = functionNode.params?.[0];
39298
- if (!isNodeOfType(firstParam, "Identifier")) return false;
39299
- const listenerParamName = firstParam.name;
39300
- let registersListener = false;
39301
- walkAst(functionNode.body, (child) => {
39302
- if (registersListener) return false;
39303
- if (!isNodeOfType(child, "CallExpression")) return;
39304
- if (!isNodeOfType(child.callee, "MemberExpression")) return;
39305
- if (!isNodeOfType(child.callee.object, "Identifier")) return;
39306
- if (!listenerCollectionNames.has(child.callee.object.name)) return;
39307
- if (!isNodeOfType(child.callee.property, "Identifier")) return;
39308
- if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39309
- const registeredArgument = child.arguments?.[0];
39310
- if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39311
- });
39312
- return registersListener;
39313
- };
39314
- const buildModuleScopeStoreIndex = (programRoot) => {
39315
- const mutableBindingNames = /* @__PURE__ */ new Set();
39316
- const listenerCollectionNames = /* @__PURE__ */ new Set();
39317
- const moduleFunctionsByName = /* @__PURE__ */ new Map();
39318
- if (!isNodeOfType(programRoot, "Program")) return {
39319
- mutableBindingNames,
39320
- subscribeFunctionNames: /* @__PURE__ */ new Set()
39321
- };
39322
- for (const statement of programRoot.body ?? []) {
39323
- const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39324
- if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39325
- moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39326
- continue;
39327
- }
39328
- if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39329
- for (const declarator of unwrapped.declarations ?? []) {
39330
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
39331
- const init = declarator.init ?? null;
39332
- if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39333
- mutableBindingNames.add(declarator.id.name);
39334
- continue;
39335
- }
39336
- if (isListenerCollectionInitializer(init)) {
39337
- listenerCollectionNames.add(declarator.id.name);
39338
- continue;
39339
- }
39340
- if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39341
- }
39342
- }
39343
- const subscribeFunctionNames = /* @__PURE__ */ new Set();
39344
- for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39345
- return {
39346
- mutableBindingNames,
39347
- subscribeFunctionNames
39348
- };
39349
- };
39350
- const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39351
- if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39352
- let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39353
- if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39354
- if (!isNodeOfType(initialArgument, "Identifier")) return null;
39355
- if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39356
- const binding = findVariableInitializer(initialArgument, initialArgument.name);
39357
- if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39358
- return initialArgument.name;
39359
- };
39360
- const argumentForwardsSetter = (argument, setterName) => {
39361
- if (!argument) return false;
39362
- const unwrapped = stripParenExpression(argument);
39363
- if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39364
- if (!isFunctionLike$1(unwrapped)) return false;
39365
- let callsSetter = false;
39366
- walkAst(unwrapped.body, (child) => {
39367
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39368
- callsSetter = true;
39369
- return false;
39370
- }
39371
- });
39372
- return callsSetter;
39373
- };
39374
- const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39375
- let matchedCall = null;
39376
- walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39377
- if (matchedCall) return false;
39378
- if (!isNodeOfType(child, "CallExpression")) return;
39379
- if (!isNodeOfType(child.callee, "Identifier")) return;
39380
- if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39381
- const binding = findVariableInitializer(child.callee, child.callee.name);
39382
- if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39383
- for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39384
- matchedCall = child;
39385
- return false;
39386
- }
39387
- });
39388
- return matchedCall;
39389
- };
39390
38490
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
39391
38491
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
39392
38492
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -39403,16 +38503,6 @@ const preferUseSyncExternalStore = defineRule({
39403
38503
  severity: "warn",
39404
38504
  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.",
39405
38505
  create: (context) => {
39406
- let cachedStoreIndex = null;
39407
- const storeIndexFor = (node) => {
39408
- if (cachedStoreIndex) return cachedStoreIndex;
39409
- const programRoot = findProgramRoot(node);
39410
- cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39411
- mutableBindingNames: /* @__PURE__ */ new Set(),
39412
- subscribeFunctionNames: /* @__PURE__ */ new Set()
39413
- };
39414
- return cachedStoreIndex;
39415
- };
39416
38506
  const checkComponent = (componentBody) => {
39417
38507
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
39418
38508
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -39450,7 +38540,6 @@ const preferUseSyncExternalStore = defineRule({
39450
38540
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
39451
38541
  if (!useStateInitializer) continue;
39452
38542
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39453
- if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
39454
38543
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
39455
38544
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
39456
38545
  context.report({
@@ -39458,47 +38547,14 @@ const preferUseSyncExternalStore = defineRule({
39458
38547
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
39459
38548
  });
39460
38549
  }
39461
- checkModuleStoreShape(componentBody, useStateBindings);
39462
- };
39463
- const checkModuleStoreShape = (componentBody, useStateBindings) => {
39464
- const storeIndex = storeIndexFor(componentBody);
39465
- if (storeIndex.mutableBindingNames.size === 0) return;
39466
- if (storeIndex.subscribeFunctionNames.size === 0) return;
39467
- const snapshotBindings = useStateBindings.map((binding) => ({
39468
- binding,
39469
- storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39470
- })).filter((candidate) => candidate.storeName !== null);
39471
- if (snapshotBindings.length === 0) return;
39472
- const reportedDeclarators = /* @__PURE__ */ new Set();
39473
- for (const effectCall of findUseEffectsInComponent(componentBody)) {
39474
- if (!isNodeOfType(effectCall, "CallExpression")) continue;
39475
- if ((effectCall.arguments?.length ?? 0) < 2) continue;
39476
- const depsNode = effectCall.arguments[1];
39477
- if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39478
- if ((depsNode.elements?.length ?? 0) !== 0) continue;
39479
- const callback = getEffectCallback(effectCall);
39480
- if (!callback || !isFunctionLike$1(callback)) continue;
39481
- for (const { binding, storeName } of snapshotBindings) {
39482
- if (reportedDeclarators.has(binding.declarator)) continue;
39483
- if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39484
- reportedDeclarators.add(binding.declarator);
39485
- context.report({
39486
- node: binding.declarator,
39487
- 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.`
39488
- });
39489
- }
39490
- }
39491
38550
  };
39492
38551
  return {
39493
38552
  FunctionDeclaration(node) {
39494
- const functionName = node.id?.name;
39495
- if (!functionName) return;
39496
- if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38553
+ if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39497
38554
  checkComponent(node.body);
39498
38555
  },
39499
38556
  VariableDeclarator(node) {
39500
- const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39501
- if (!isComponentAssignment(node) && !isHookAssignment) return;
38557
+ if (!isComponentAssignment(node)) return;
39502
38558
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
39503
38559
  checkComponent(node.init.body);
39504
38560
  }
@@ -40108,7 +39164,7 @@ const queryNoVoidQueryFn = defineRule({
40108
39164
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
40109
39165
  const body = queryFnValue.body;
40110
39166
  if (!isNodeOfType(body, "BlockStatement")) return;
40111
- if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39167
+ if ((body.body ?? []).length === 0) context.report({
40112
39168
  node: queryFnProperty,
40113
39169
  message: "This empty queryFn caches undefined, so the component never gets data."
40114
39170
  });
@@ -40615,6 +39671,17 @@ const renderingHoistJsx = defineRule({
40615
39671
  }
40616
39672
  });
40617
39673
  //#endregion
39674
+ //#region src/plugin/utils/find-declarator-for-binding.ts
39675
+ const findDeclaratorForBinding = (bindingIdentifier) => {
39676
+ let ancestor = bindingIdentifier.parent;
39677
+ while (ancestor) {
39678
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39679
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39680
+ ancestor = ancestor.parent ?? null;
39681
+ }
39682
+ return null;
39683
+ };
39684
+ //#endregion
40618
39685
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
40619
39686
  const NONDETERMINISTIC_RENDER_PATTERNS = [
40620
39687
  {
@@ -40623,19 +39690,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
40623
39690
  },
40624
39691
  {
40625
39692
  display: "Date.now()",
40626
- matches: (node) => isGlobalMethodCall(node, "Date", "now")
39693
+ matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40627
39694
  },
40628
39695
  {
40629
39696
  display: "Math.random()",
40630
- matches: (node) => isGlobalMethodCall(node, "Math", "random")
39697
+ matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
40631
39698
  },
40632
39699
  {
40633
39700
  display: "performance.now()",
40634
- matches: (node) => isGlobalMethodCall(node, "performance", "now")
39701
+ matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40635
39702
  },
40636
39703
  {
40637
39704
  display: "crypto.randomUUID()",
40638
- matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39705
+ matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
40639
39706
  }
40640
39707
  ];
40641
39708
  const findOpeningElementOfChild = (jsxNode) => {
@@ -40647,6 +39714,54 @@ const findOpeningElementOfChild = (jsxNode) => {
40647
39714
  }
40648
39715
  return null;
40649
39716
  };
39717
+ const executesDuringRender = (functionNode) => {
39718
+ const parent = functionNode.parent;
39719
+ if (!isNodeOfType(parent, "CallExpression")) return false;
39720
+ if (parent.callee === functionNode) return true;
39721
+ return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39722
+ };
39723
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39724
+ const referencesClientOnlyFlag = (expression) => {
39725
+ const unwrapped = stripParenExpression(expression);
39726
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39727
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
39728
+ const property = unwrapped.property;
39729
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39730
+ }
39731
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39732
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39733
+ return false;
39734
+ };
39735
+ const isFalsyLiteral = (node) => {
39736
+ if (!node) return true;
39737
+ if (isNodeOfType(node, "Literal")) return !node.value;
39738
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
39739
+ };
39740
+ const isFalsyInitialStateBinding = (identifier) => {
39741
+ if (!isNodeOfType(identifier, "Identifier")) return false;
39742
+ const binding = findVariableInitializer(identifier, identifier.name);
39743
+ if (!binding) return false;
39744
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39745
+ if (!declarator?.init) return false;
39746
+ const init = stripParenExpression(declarator.init);
39747
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39748
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39749
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39750
+ return isFalsyLiteral(init.arguments?.[0]);
39751
+ };
39752
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39753
+ const isGatedByFalsyInitialState = (node) => {
39754
+ let cursor = node;
39755
+ let parent = node.parent;
39756
+ while (parent) {
39757
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39758
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39759
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39760
+ cursor = parent;
39761
+ parent = parent.parent ?? null;
39762
+ }
39763
+ return false;
39764
+ };
40650
39765
  const isYearOnlyDateRead = (dateNode) => {
40651
39766
  const member = dateNode.parent;
40652
39767
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -40670,6 +39785,23 @@ const isInsideMotionTransitionAttribute = (node) => {
40670
39785
  }
40671
39786
  return false;
40672
39787
  };
39788
+ const isInsideClientOnlyGuard = (node) => {
39789
+ let cursor = node;
39790
+ let parent = node.parent;
39791
+ while (parent) {
39792
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
39793
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
39794
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
39795
+ cursor = parent;
39796
+ parent = parent.parent ?? null;
39797
+ }
39798
+ return false;
39799
+ };
39800
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
39801
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
39802
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
39803
+ return false;
39804
+ };
40673
39805
  const renderingHydrationMismatchTime = defineRule({
40674
39806
  id: "rendering-hydration-mismatch-time",
40675
39807
  title: "Time or random value in JSX",
@@ -40717,35 +39849,6 @@ const renderingHydrationMismatchTime = defineRule({
40717
39849
  }
40718
39850
  });
40719
39851
  //#endregion
40720
- //#region src/plugin/utils/contains-locale-environment-read.ts
40721
- const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40722
- "toLocaleString",
40723
- "toLocaleDateString",
40724
- "toLocaleTimeString",
40725
- "getTimezoneOffset"
40726
- ]);
40727
- const containsLocaleEnvironmentRead = (expression) => {
40728
- let readsLocaleEnvironment = false;
40729
- walkAst(expression, (child) => {
40730
- if (readsLocaleEnvironment) return false;
40731
- if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40732
- if (child.object.name === "Intl") {
40733
- readsLocaleEnvironment = true;
40734
- return false;
40735
- }
40736
- if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40737
- readsLocaleEnvironment = true;
40738
- return false;
40739
- }
40740
- }
40741
- 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)) {
40742
- readsLocaleEnvironment = true;
40743
- return false;
40744
- }
40745
- });
40746
- return readsLocaleEnvironment;
40747
- };
40748
- //#endregion
40749
39852
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
40750
39853
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
40751
39854
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -40811,15 +39914,14 @@ const renderingHydrationNoFlicker = defineRule({
40811
39914
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
40812
39915
  const callback = getEffectCallback(node);
40813
39916
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
40814
- const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
40815
- if (bodyStatements.length !== 1) return;
39917
+ const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
39918
+ if (!bodyStatements || bodyStatements.length !== 1) return;
40816
39919
  const soleStatement = bodyStatements[0];
40817
39920
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
40818
39921
  const expression = soleStatement.expression;
40819
39922
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
40820
39923
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
40821
39924
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
40822
- if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
40823
39925
  context.report({
40824
39926
  node,
40825
39927
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -41638,9 +40740,6 @@ const rerenderFunctionalSetstate = defineRule({
41638
40740
  } })
41639
40741
  });
41640
40742
  //#endregion
41641
- //#region src/plugin/utils/is-trivial-built-in-construction.ts
41642
- const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41643
- //#endregion
41644
40743
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
41645
40744
  const rerenderLazyRefInit = defineRule({
41646
40745
  id: "rerender-lazy-ref-init",
@@ -41651,7 +40750,7 @@ const rerenderLazyRefInit = defineRule({
41651
40750
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
41652
40751
  create: (context) => ({ CallExpression(node) {
41653
40752
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
41654
- const initializer = stripParenExpression(node.arguments[0]);
40753
+ const initializer = node.arguments[0];
41655
40754
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
41656
40755
  const isNewCall = isNodeOfType(initializer, "NewExpression");
41657
40756
  if (!isPlainCall && !isNewCall) return;
@@ -41659,7 +40758,7 @@ const rerenderLazyRefInit = defineRule({
41659
40758
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
41660
40759
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
41661
40760
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
41662
- if (isTrivialBuiltInConstruction(initializer)) return;
40761
+ if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41663
40762
  if (isPlainCall && isReactHookName(calleeName)) return;
41664
40763
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
41665
40764
  context.report({
@@ -41695,11 +40794,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
41695
40794
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
41696
40795
  const findEagerInitializerCall = (expression, depth = 0) => {
41697
40796
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
41698
- const innerExpression = stripParenExpression(expression);
41699
- if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41700
- if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41701
- if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41702
- if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
40797
+ if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
40798
+ if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
40799
+ if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
40800
+ if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
40801
+ if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41703
40802
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
41704
40803
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
41705
40804
  if (spreadCall) return spreadCall;
@@ -41722,7 +40821,7 @@ const rerenderLazyStateInit = defineRule({
41722
40821
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
41723
40822
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
41724
40823
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
41725
- if (isTrivialBuiltInConstruction(initializer)) return;
40824
+ if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41726
40825
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
41727
40826
  if (isReactHookName(calleeName)) return;
41728
40827
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -42389,7 +41488,7 @@ const rnAnimationReactionAsDerived = defineRule({
42389
41488
  const body = reactionFn.body;
42390
41489
  let singleAssignment = null;
42391
41490
  if (isNodeOfType(body, "BlockStatement")) {
42392
- const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41491
+ const statements = body.body ?? [];
42393
41492
  if (statements.length !== 1) return;
42394
41493
  const onlyStatement = statements[0];
42395
41494
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -42463,8 +41562,7 @@ const PROMISE_SETTLE_METHODS = new Set([
42463
41562
  "catch",
42464
41563
  "finally"
42465
41564
  ]);
42466
- const findChainRoot = (wrappedNode) => {
42467
- const node = stripParenExpression(wrappedNode);
41565
+ const findChainRoot = (node) => {
42468
41566
  if (isNodeOfType(node, "CallExpression")) {
42469
41567
  if (isNodeOfType(node.callee, "Identifier")) return {
42470
41568
  calleeName: node.callee.name,
@@ -43052,21 +42150,20 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
43052
42150
  };
43053
42151
  const isReactNativeDimensionsCallee = (node, callee) => {
43054
42152
  if (!isNodeOfType(callee, "MemberExpression")) return false;
43055
- const receiver = stripParenExpression(callee.object);
43056
- if (isNodeOfType(receiver, "Identifier")) {
43057
- const localName = receiver.name;
42153
+ if (isNodeOfType(callee.object, "Identifier")) {
42154
+ const localName = callee.object.name;
43058
42155
  const binding = findVariableInitializer(node, localName);
43059
42156
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
43060
42157
  return localName === "Dimensions";
43061
42158
  }
43062
- if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
43063
- if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
43064
- const rootName = getRootIdentifierName(receiver.object);
42159
+ if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42160
+ if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42161
+ const rootName = getRootIdentifierName(callee.object.object);
43065
42162
  if (rootName === null) return false;
43066
42163
  const importBinding = getImportBindingForName(node, rootName);
43067
42164
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
43068
42165
  }
43069
- if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42166
+ if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
43070
42167
  return false;
43071
42168
  };
43072
42169
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -43510,8 +42607,7 @@ const rnNoLegacyShadowStyles = defineRule({
43510
42607
  },
43511
42608
  CallExpression(node) {
43512
42609
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
43513
- const receiver = stripParenExpression(node.callee.object);
43514
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42610
+ if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43515
42611
  if (!isMemberProperty(node.callee, "create")) return;
43516
42612
  const stylesArgument = node.arguments?.[0];
43517
42613
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -44361,7 +43457,7 @@ const rnNoSetNativeProps = defineRule({
44361
43457
  const callee = node.callee;
44362
43458
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
44363
43459
  if (!isNodeOfType(callee, "MemberExpression")) return;
44364
- if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43460
+ if (!isStaticMemberNamed(callee.object, "current")) return;
44365
43461
  context.report({
44366
43462
  node,
44367
43463
  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."
@@ -44589,15 +43685,14 @@ const analyzeGestureChain = (expression) => {
44589
43685
  if (!isNodeOfType(callee, "MemberExpression")) return null;
44590
43686
  if (!isNodeOfType(callee.property, "Identifier")) return null;
44591
43687
  const methodName = callee.property.name;
44592
- const receiver = stripParenExpression(callee.object);
44593
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43688
+ if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44594
43689
  factoryName: methodName,
44595
43690
  chainMethodNames,
44596
43691
  numberOfTapsArgument
44597
43692
  };
44598
43693
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
44599
43694
  chainMethodNames.push(methodName);
44600
- cursor = receiver;
43695
+ cursor = callee.object;
44601
43696
  }
44602
43697
  return null;
44603
43698
  };
@@ -45122,9 +44217,9 @@ const roleHasRequiredAriaProps = defineRule({
45122
44217
  if (!HTML_TAGS.has(elementType)) return;
45123
44218
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
45124
44219
  if (!roleAttribute) return;
45125
- const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
45126
- if (roleCandidates === null) return;
45127
- const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44220
+ const roleValue = getJsxPropStringValue(roleAttribute);
44221
+ if (roleValue === null) return;
44222
+ const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
45128
44223
  for (const role of roles) {
45129
44224
  const required = ROLE_REQUIRED_PROPS.get(role);
45130
44225
  if (!required) continue;
@@ -48241,9 +47336,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
48241
47336
  };
48242
47337
  //#endregion
48243
47338
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
48244
- const buildMessageDefault = (roles, propName) => {
48245
- 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.`;
48246
- };
47339
+ const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
48247
47340
  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.`;
48248
47341
  const roleSupportsAriaProps = defineRule({
48249
47342
  id: "role-supports-aria-props",
@@ -48263,8 +47356,6 @@ const roleSupportsAriaProps = defineRule({
48263
47356
  const propName = propRawName.toLowerCase();
48264
47357
  if (!propName.startsWith("aria-")) continue;
48265
47358
  if (!ARIA_PROPERTIES.has(propName)) continue;
48266
- const attributeValue = attributeNode.value;
48267
- if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
48268
47359
  (ariaAttributes ??= []).push({
48269
47360
  attribute,
48270
47361
  propName
@@ -48273,21 +47364,17 @@ const roleSupportsAriaProps = defineRule({
48273
47364
  if (!ariaAttributes) return;
48274
47365
  const elementType = getElementType(node, context.settings);
48275
47366
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
48276
- const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
48277
- if (roleCandidates === null || roleCandidates.length === 0) return;
48278
- const supportedSets = [];
48279
- for (const role of roleCandidates) {
48280
- if (!VALID_ARIA_ROLES.has(role)) return;
48281
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
48282
- if (!supported) return;
48283
- supportedSets.push(supported);
48284
- }
47367
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47368
+ if (!role) return;
47369
+ if (!VALID_ARIA_ROLES.has(role)) return;
48285
47370
  const isImplicit = !roleAttribute;
47371
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47372
+ if (!supported) return;
48286
47373
  for (const { attribute, propName } of ariaAttributes) {
48287
- if (supportedSets.some((supported) => supported.has(propName))) continue;
47374
+ if (supported.has(propName)) continue;
48288
47375
  context.report({
48289
47376
  node: attribute,
48290
- message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47377
+ message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
48291
47378
  });
48292
47379
  }
48293
47380
  } })
@@ -48634,6 +47721,21 @@ const isInsideLoop = (descendant, ancestor) => {
48634
47721
  }
48635
47722
  return false;
48636
47723
  };
47724
+ const isUseEffectEventSymbol = (symbol) => {
47725
+ const initializer = symbol.initializer;
47726
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47727
+ return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47728
+ };
47729
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47730
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47731
+ return Boolean(symbol && symbol.kind !== "import");
47732
+ };
47733
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47734
+ const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47735
+ const initializer = symbol.initializer;
47736
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47737
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47738
+ };
48637
47739
  const findEnclosingComponentOrHookFunction = (node) => {
48638
47740
  let current = node.parent;
48639
47741
  while (current) {
@@ -48823,7 +47925,8 @@ const rulesOfHooks = defineRule({
48823
47925
  },
48824
47926
  Identifier(node) {
48825
47927
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
48826
- if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
47928
+ if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47929
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
48827
47930
  if (!isSameComponentOrHookScope(symbol, node)) return;
48828
47931
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
48829
47932
  context.report({
@@ -48971,8 +48074,7 @@ const serverAfterNonblocking = defineRule({
48971
48074
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48972
48075
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48973
48076
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48974
- const receiver = stripParenExpression(node.callee.object);
48975
- const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48077
+ const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
48976
48078
  if (!objectName) return;
48977
48079
  const methodName = node.callee.property.name;
48978
48080
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -49648,17 +48750,7 @@ const getMemberPropertyName$1 = (memberExpression) => {
49648
48750
  };
49649
48751
  const ascendMemberChain = (referenceIdentifier) => {
49650
48752
  let chainTip = referenceIdentifier;
49651
- while (chainTip.parent) {
49652
- if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49653
- chainTip = chainTip.parent;
49654
- continue;
49655
- }
49656
- if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49657
- chainTip = chainTip.parent;
49658
- continue;
49659
- }
49660
- break;
49661
- }
48753
+ while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49662
48754
  return chainTip;
49663
48755
  };
49664
48756
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -49683,8 +48775,7 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
49683
48775
  const callee = callExpression.callee;
49684
48776
  if (isNodeOfType(callee, "MemberExpression")) {
49685
48777
  const methodName = getMemberPropertyName$1(callee);
49686
- const calleeReceiver = stripParenExpression(callee.object);
49687
- return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
48778
+ return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49688
48779
  }
49689
48780
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
49690
48781
  const calleeSymbol = scopes.symbolFor(callee);
@@ -50414,7 +49505,7 @@ const walkServerFnChain = (outerNode) => {
50414
49505
  };
50415
49506
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
50416
49507
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
50417
- let currentNode = stripParenExpression(outerNode.callee.object);
49508
+ let currentNode = outerNode.callee.object;
50418
49509
  while (isNodeOfType(currentNode, "CallExpression")) {
50419
49510
  const calleeName = getCalleeName$2(currentNode);
50420
49511
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -50425,7 +49516,7 @@ const walkServerFnChain = (outerNode) => {
50425
49516
  }
50426
49517
  }
50427
49518
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
50428
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49519
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50429
49520
  else break;
50430
49521
  }
50431
49522
  return result;
@@ -51078,7 +50169,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
51078
50169
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
51079
50170
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
51080
50171
  if (methodName) methodNames.unshift(methodName);
51081
- currentNode = stripParenExpression(currentNode.callee.object);
50172
+ currentNode = currentNode.callee.object;
51082
50173
  }
51083
50174
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
51084
50175
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -54160,18 +53251,6 @@ const reactDoctorRules = [
54160
53251
  category: "Bugs"
54161
53252
  }
54162
53253
  },
54163
- {
54164
- key: "react-doctor/no-locale-format-in-render",
54165
- id: "no-locale-format-in-render",
54166
- source: "react-doctor",
54167
- originallyExternal: false,
54168
- rule: {
54169
- ...noLocaleFormatInRender,
54170
- framework: "global",
54171
- category: "Bugs",
54172
- requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54173
- }
54174
- },
54175
53254
  {
54176
53255
  key: "react-doctor/no-long-transition-duration",
54177
53256
  id: "no-long-transition-duration",
@@ -57048,7 +56127,6 @@ const CROSS_FILE_RULE_IDS = new Set([
57048
56127
  "nextjs-no-use-search-params-without-suspense",
57049
56128
  "no-dynamic-import-path",
57050
56129
  "no-full-lodash-import",
57051
- "no-locale-format-in-render",
57052
56130
  "no-mutating-reducer-state",
57053
56131
  "prefer-dynamic-import",
57054
56132
  "rendering-hydration-mismatch-time",
@@ -57183,7 +56261,6 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
57183
56261
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
57184
56262
  ["no-dynamic-import-path", collectNearestManifestDependencies],
57185
56263
  ["no-full-lodash-import", collectNearestManifestDependencies],
57186
- ["no-locale-format-in-render", collectNearestManifestDependencies],
57187
56264
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
57188
56265
  ["prefer-dynamic-import", collectNearestManifestDependencies],
57189
56266
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],