oxlint-plugin-react-doctor 0.7.2-dev.82e0475 → 0.7.2-dev.9b59d96

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.
package/dist/index.js CHANGED
@@ -2277,6 +2277,7 @@ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2277
2277
  if (remainingHops === 0) return null;
2278
2278
  const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2279
2279
  if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
2280
2281
  return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2281
2282
  }
2282
2283
  return null;
@@ -2316,7 +2317,8 @@ const isDirectChildOfLinkComponent = (openingElement) => {
2316
2317
  const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
2317
2318
  const isInvalidHref = (value, validHrefs) => {
2318
2319
  if (validHrefs.has(value)) return false;
2319
- return value === "" || value === "#" || value === "javascript:void(0)";
2320
+ const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
2321
+ return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
2320
2322
  };
2321
2323
  const isNullishOrFragmentHref = (value) => {
2322
2324
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -5191,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
5191
5193
  const callee = node.callee;
5192
5194
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
5193
5195
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
5194
- if (!isWebStorageObject(callee.object)) return;
5196
+ if (!isWebStorageObject(stripParenExpression(callee.object))) return;
5195
5197
  const keyArgument = node.arguments?.[0];
5196
5198
  if (!keyArgument) return;
5197
5199
  const keyString = resolveStaticKeyString(keyArgument);
@@ -6131,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
6131
6133
  })
6132
6134
  });
6133
6135
  //#endregion
6136
+ //#region src/plugin/utils/is-global-method-call.ts
6137
+ const isGlobalMethodCall = (node, objectName, methodName) => {
6138
+ if (!isNodeOfType(node, "CallExpression")) return false;
6139
+ const callee = stripParenExpression(node.callee);
6140
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
6141
+ const receiver = stripParenExpression(callee.object);
6142
+ return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
6143
+ };
6144
+ //#endregion
6134
6145
  //#region src/plugin/rules/client/client-localstorage-no-version.ts
6135
6146
  const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
6136
6147
  const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
6137
6148
  const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
6138
6149
  const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
6139
- const isJsonStringifyCall = (node) => {
6140
- if (!isNodeOfType(node, "CallExpression")) return false;
6141
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
6142
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
6143
- if (node.callee.object.name !== "JSON") return false;
6144
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
6145
- return node.callee.property.name === "stringify";
6146
- };
6150
+ const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
6147
6151
  const resolveStringKey = (keyArg, context) => {
6148
6152
  if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
6149
6153
  if (!isNodeOfType(keyArg, "Identifier")) return null;
@@ -6162,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
6162
6166
  recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
6163
6167
  create: (context) => ({ CallExpression(node) {
6164
6168
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
6165
- if (!isNodeOfType(node.callee.object, "Identifier")) return;
6166
- if (!STORAGE_OBJECTS.has(node.callee.object.name)) return;
6169
+ const receiver = stripParenExpression(node.callee.object);
6170
+ if (!isNodeOfType(receiver, "Identifier")) return;
6171
+ if (!STORAGE_OBJECTS.has(receiver.name)) return;
6167
6172
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
6168
6173
  if (node.callee.property.name !== "setItem") return;
6169
6174
  const keyArg = node.arguments?.[0];
@@ -6176,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
6176
6181
  if (!isJsonStringifyCall(valueArg)) return;
6177
6182
  context.report({
6178
6183
  node: keyArg,
6179
- message: `${node.callee.object.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6184
+ message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
6180
6185
  });
6181
6186
  } })
6182
6187
  });
@@ -7718,6 +7723,7 @@ const isResultDiscardedCall = (callExpression) => {
7718
7723
  let parent = node.parent;
7719
7724
  while (parent) {
7720
7725
  if (isNodeOfType(parent, "ExpressionStatement")) return true;
7726
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
7721
7727
  if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7722
7728
  if (isNodeOfType(parent, "ChainExpression")) {
7723
7729
  node = parent;
@@ -7952,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
7952
7958
  };
7953
7959
  if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
7954
7960
  if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
7961
+ walkAst(effectCallback.body, (child) => {
7962
+ if (!isSubscribeOrObserveCall(child)) return;
7963
+ if (!isNodeOfType(child, "CallExpression")) return;
7964
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
7965
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
7966
+ bindings.subscriptionNames.add(child.callee.object.name);
7967
+ });
7955
7968
  walkInsideStatementBlocks(effectCallback.body, (child) => {
7956
7969
  if (!isNodeOfType(child, "VariableDeclaration")) return;
7957
7970
  for (const declarator of child.declarations ?? []) {
@@ -7994,6 +8007,22 @@ const getRangeStart = (node) => {
7994
8007
  const rangeStart = node.range?.[0];
7995
8008
  return typeof rangeStart === "number" ? rangeStart : null;
7996
8009
  };
8010
+ const removeSynchronouslyReleasedUsages = (callback, usages) => {
8011
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8012
+ if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
8013
+ const releaseStarts = [];
8014
+ walkInsideStatementBlocks(callback.body, (child) => {
8015
+ if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
8016
+ const releaseStart = getRangeStart(child);
8017
+ if (releaseStart !== null) releaseStarts.push(releaseStart);
8018
+ });
8019
+ if (releaseStarts.length === 0) return usages;
8020
+ return usages.filter((usage) => {
8021
+ const usageStart = getRangeStart(usage.node);
8022
+ if (usageStart === null) return true;
8023
+ return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
8024
+ });
8025
+ };
7997
8026
  const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
7998
8027
  if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
7999
8028
  const returnStart = getRangeStart(returnStatement);
@@ -8017,7 +8046,14 @@ const effectHasCleanupReturn = (callback, usages) => {
8017
8046
  return didFindCleanupReturn;
8018
8047
  };
8019
8048
  const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
8020
- const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && (property.key.name === "signal" || property.key.name === "once" && isNodeOfType(property.value, "Literal") && property.value.value === true)));
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));
8021
8057
  const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8022
8058
  ["addEventListener", new Set(["removeEventListener", "abort"])],
8023
8059
  ["addListener", new Set([
@@ -8092,10 +8128,12 @@ const findRetainedFunctionLeak = (retainedFunction) => {
8092
8128
  if (!isFunctionLike$1(retainedFunction)) return null;
8093
8129
  const body = retainedFunction.body;
8094
8130
  if (!body) return null;
8131
+ const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
8095
8132
  let leak = null;
8096
8133
  walkAst(body, (child) => {
8097
8134
  if (leak !== null) return false;
8098
8135
  if (isFunctionLike$1(child)) return false;
8136
+ if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
8099
8137
  if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8100
8138
  leak = {
8101
8139
  kind: "socket",
@@ -8157,7 +8195,7 @@ const effectNeedsCleanup = defineRule({
8157
8195
  if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8158
8196
  const callback = getEffectCallback(node);
8159
8197
  if (!callback) return;
8160
- const usages = findSubscribeLikeUsages(callback);
8198
+ const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
8161
8199
  if (usages.length === 0) return;
8162
8200
  if (effectHasCleanupReturn(callback, usages)) return;
8163
8201
  const firstUsage = usages[0];
@@ -10172,7 +10210,10 @@ const PRAGMA = "React";
10172
10210
  const isReactFunctionCall = (node, expectedCall) => {
10173
10211
  if (!isNodeOfType(node, "CallExpression")) return false;
10174
10212
  if (getCalleeName$2(node) !== expectedCall) return false;
10175
- if (isNodeOfType(node.callee, "MemberExpression")) return isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === PRAGMA;
10213
+ if (isNodeOfType(node.callee, "MemberExpression")) {
10214
+ const receiver = stripParenExpression(node.callee.object);
10215
+ return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
10216
+ }
10176
10217
  return true;
10177
10218
  };
10178
10219
  //#endregion
@@ -12249,14 +12290,15 @@ const jsCacheStorage = defineRule({
12249
12290
  "ArrowFunctionExpression:exit": exitFunctionScope,
12250
12291
  CallExpression(node) {
12251
12292
  if (!isMemberProperty(node.callee, "getItem")) return;
12252
- if (!isNodeOfType(node.callee.object, "Identifier") || !STORAGE_OBJECTS$1.has(node.callee.object.name)) return;
12293
+ const receiver = stripParenExpression(node.callee.object);
12294
+ if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
12253
12295
  if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
12254
12296
  const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
12255
12297
  const storageKey = String(node.arguments[0].value);
12256
12298
  const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
12257
12299
  storageReadCounts.set(storageKey, readCount);
12258
12300
  if (readCount === 2) {
12259
- const storageName = node.callee.object.name;
12301
+ const storageName = receiver.name;
12260
12302
  context.report({
12261
12303
  node,
12262
12304
  message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
@@ -12270,13 +12312,16 @@ const jsCacheStorage = defineRule({
12270
12312
  //#region src/plugin/rules/js-performance/js-combine-iterations.ts
12271
12313
  const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
12272
12314
  const callee = callExpression.callee;
12273
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12274
- if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12275
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12276
- const receiver = callee.object;
12277
- if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12278
- return true;
12315
+ if (isNodeOfType(callee, "MemberExpression")) {
12316
+ const receiver = stripParenExpression(callee.object);
12317
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
12318
+ if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
12319
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
12320
+ return true;
12321
+ }
12322
+ return false;
12279
12323
  }
12324
+ if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
12280
12325
  return false;
12281
12326
  };
12282
12327
  const isChainPassThroughCall = (callExpression) => {
@@ -12288,10 +12333,7 @@ const isChainPassThroughCall = (callExpression) => {
12288
12333
  const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
12289
12334
  let cursor = receiverNode;
12290
12335
  while (cursor) {
12291
- if (isNodeOfType(cursor, "ChainExpression")) {
12292
- cursor = cursor.expression;
12293
- continue;
12294
- }
12336
+ cursor = stripParenExpression(cursor);
12295
12337
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12296
12338
  if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
12297
12339
  if (!isChainPassThroughCall(cursor)) return false;
@@ -12367,10 +12409,7 @@ const isStringSplitRootedChain = (receiverNode) => {
12367
12409
  let hops = 0;
12368
12410
  while (cursor && hops < 12) {
12369
12411
  hops += 1;
12370
- if (isNodeOfType(cursor, "ChainExpression")) {
12371
- cursor = cursor.expression;
12372
- continue;
12373
- }
12412
+ cursor = stripParenExpression(cursor);
12374
12413
  if (!isNodeOfType(cursor, "CallExpression")) return false;
12375
12414
  const callee = cursor.callee;
12376
12415
  if (!isNodeOfType(callee, "MemberExpression")) return false;
@@ -12394,10 +12433,7 @@ const isSmallLiteralArray = (node) => {
12394
12433
  const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
12395
12434
  let cursor = receiverNode;
12396
12435
  while (cursor) {
12397
- if (isNodeOfType(cursor, "ChainExpression")) {
12398
- cursor = cursor.expression;
12399
- continue;
12400
- }
12436
+ cursor = stripParenExpression(cursor);
12401
12437
  if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
12402
12438
  if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
12403
12439
  if (!isNodeOfType(cursor, "CallExpression")) return false;
@@ -12462,7 +12498,7 @@ const jsCombineIterations = defineRule({
12462
12498
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
12463
12499
  const outerMethod = node.callee.property.name;
12464
12500
  if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
12465
- const innerCall = node.callee.object;
12501
+ const innerCall = stripParenExpression(node.callee.object);
12466
12502
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12467
12503
  const innerMethod = innerCall.callee.property.name;
12468
12504
  if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
@@ -12535,10 +12571,10 @@ const jsFlatmapFilter = defineRule({
12535
12571
  if (!filterArgument) return;
12536
12572
  const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
12537
12573
  if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
12538
- const innerCall = node.callee.object;
12574
+ const innerCall = stripParenExpression(node.callee.object);
12539
12575
  if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
12540
12576
  if (innerCall.callee.property.name !== "map") return;
12541
- const receiver = innerCall.callee.object;
12577
+ const receiver = stripParenExpression(innerCall.callee.object);
12542
12578
  if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
12543
12579
  const elements = receiver.elements ?? [];
12544
12580
  if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
@@ -13798,7 +13834,7 @@ const jsTosortedImmutable = defineRule({
13798
13834
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13799
13835
  create: (context) => ({ CallExpression(node) {
13800
13836
  if (!isMemberProperty(node.callee, "sort")) return;
13801
- const receiver = node.callee.object;
13837
+ const receiver = stripParenExpression(node.callee.object);
13802
13838
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
13803
13839
  const spreadArgument = receiver.elements[0].argument;
13804
13840
  if (isFreshOrIteratorAllocation(spreadArgument)) return;
@@ -18835,7 +18871,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
18835
18871
  };
18836
18872
  const describeClientSideNavigation = (node) => {
18837
18873
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
18838
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
18874
+ const receiver = stripParenExpression(node.callee.object);
18875
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
18839
18876
  const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
18840
18877
  if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
18841
18878
  }
@@ -19455,7 +19492,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
19455
19492
  if (!isNodeOfType(node.callee, "MemberExpression")) return null;
19456
19493
  if (!isNodeOfType(node.callee.property, "Identifier")) return null;
19457
19494
  if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
19458
- if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
19495
+ if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
19459
19496
  return node.callee.property.name;
19460
19497
  };
19461
19498
  const isMutatingFetchCall = (node) => {
@@ -19469,7 +19506,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
19469
19506
  if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
19470
19507
  const { property, object } = node.callee;
19471
19508
  if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
19472
- if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
19509
+ if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
19473
19510
  return true;
19474
19511
  };
19475
19512
  const getDbCallDescription = (node) => {
@@ -19477,7 +19514,8 @@ const getDbCallDescription = (node) => {
19477
19514
  if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
19478
19515
  if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
19479
19516
  const methodName = node.callee.property.name;
19480
- const rootObjectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
19517
+ const receiver = stripParenExpression(node.callee.object);
19518
+ const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
19481
19519
  return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
19482
19520
  };
19483
19521
  const findSideEffect = (node, options = {}) => {
@@ -20877,13 +20915,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
20877
20915
  if (isNodeOfType(current, "CallExpression")) {
20878
20916
  let node = ref.identifier;
20879
20917
  let parent = node.parent;
20880
- while (parent && isNodeOfType(parent, "MemberExpression")) {
20918
+ while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
20881
20919
  node = parent;
20882
20920
  parent = node.parent;
20883
20921
  }
20884
20922
  if (current.callee === node) return current;
20885
20923
  }
20886
- if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
20924
+ if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
20887
20925
  return null;
20888
20926
  };
20889
20927
  const getArgsUpstreamRefs = (analysis, ref) => {
@@ -21018,7 +21056,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
21018
21056
  const importDeclaration = declarationNode.parent;
21019
21057
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
21020
21058
  }));
21021
- const isHookCallee = (analysis, node, hookName) => {
21059
+ const isHookCallee$1 = (analysis, node, hookName) => {
21022
21060
  if (!node) return false;
21023
21061
  if (isNodeOfType(node, "Identifier")) {
21024
21062
  if (node.name === hookName) return true;
@@ -21027,15 +21065,19 @@ const isHookCallee = (analysis, node, hookName) => {
21027
21065
  if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
21028
21066
  return false;
21029
21067
  }
21030
- if (isNodeOfType(node, "MemberExpression")) return isNodeOfType(node.object, "Identifier") && node.object.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21068
+ if (isNodeOfType(node, "MemberExpression")) {
21069
+ const receiver = stripParenExpression(node.object);
21070
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
21071
+ }
21031
21072
  return false;
21032
21073
  };
21033
21074
  const isUseEffect = (node) => {
21034
21075
  if (!node || !isNodeOfType(node, "CallExpression")) return false;
21035
21076
  const callee = node.callee;
21036
21077
  if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
21037
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect") return true;
21038
- return false;
21078
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
21079
+ const receiver = stripParenExpression(callee.object);
21080
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
21039
21081
  };
21040
21082
  const getEffectFn = (analysis, node) => {
21041
21083
  if (!isNodeOfType(node, "CallExpression")) return null;
@@ -21063,7 +21105,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
21063
21105
  const node = def.node;
21064
21106
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
21065
21107
  if (!isNodeOfType(node.init, "CallExpression")) return false;
21066
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21108
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
21067
21109
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
21068
21110
  const elements = node.id.elements ?? [];
21069
21111
  if (elements.length !== 1 && elements.length !== 2) return false;
@@ -21074,7 +21116,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
21074
21116
  const node = def.node;
21075
21117
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
21076
21118
  if (!isNodeOfType(node.init, "CallExpression")) return false;
21077
- if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
21119
+ if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
21078
21120
  if (!isNodeOfType(node.id, "ArrayPattern")) return false;
21079
21121
  const elements = node.id.elements ?? [];
21080
21122
  if (elements.length !== 2) return false;
@@ -21121,7 +21163,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
21121
21163
  const node = def.node;
21122
21164
  if (!isNodeOfType(node, "VariableDeclarator")) return false;
21123
21165
  if (!isNodeOfType(node.init, "CallExpression")) return false;
21124
- return isHookCallee(analysis, node.init.callee, "useRef");
21166
+ return isHookCallee$1(analysis, node.init.callee, "useRef");
21125
21167
  }));
21126
21168
  const isRefCurrent = (ref) => {
21127
21169
  const parent = ref.identifier.parent;
@@ -21134,11 +21176,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
21134
21176
  const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
21135
21177
  const isPropCallbackInvocationRef = (analysis, ref) => {
21136
21178
  if (!isPropAlias(analysis, ref)) return false;
21137
- const identifier = ref.identifier;
21138
- const parent = identifier.parent;
21179
+ let effectiveNode = ref.identifier;
21180
+ let parent = effectiveNode.parent;
21181
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
21182
+ effectiveNode = parent;
21183
+ parent = effectiveNode.parent;
21184
+ }
21139
21185
  if (!parent) return false;
21140
- if (isNodeOfType(parent, "CallExpression") && parent.callee === identifier) return true;
21141
- if (isNodeOfType(parent, "MemberExpression") && parent.object === identifier) {
21186
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
21187
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
21142
21188
  const memberParent = parent.parent;
21143
21189
  if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
21144
21190
  if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
@@ -21149,7 +21195,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
21149
21195
  };
21150
21196
  const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
21151
21197
  const getUseStateDecl = (analysis, ref) => {
21152
- let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
21198
+ let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
21153
21199
  while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
21154
21200
  return node ?? null;
21155
21201
  };
@@ -21395,7 +21441,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
21395
21441
  "summary",
21396
21442
  "textarea"
21397
21443
  ]);
21444
+ const isStaticallyFalseBooleanAttribute = (attribute) => {
21445
+ const value = attribute.value;
21446
+ if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
21447
+ const expression = value.expression;
21448
+ return isNodeOfType(expression, "Literal") && expression.value === false;
21449
+ };
21450
+ const DISABLEABLE_TAGS = new Set([
21451
+ "button",
21452
+ "input",
21453
+ "select",
21454
+ "textarea"
21455
+ ]);
21398
21456
  const isNativelyFocusable = (tagName, openingElement) => {
21457
+ if (DISABLEABLE_TAGS.has(tagName)) {
21458
+ const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
21459
+ if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
21460
+ }
21399
21461
  if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
21400
21462
  switch (tagName) {
21401
21463
  case "input": {
@@ -21409,7 +21471,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
21409
21471
  case "a":
21410
21472
  case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
21411
21473
  case "audio":
21412
- case "video": return hasJsxPropIgnoreCase(openingElement.attributes, "controls") !== void 0;
21474
+ case "video": {
21475
+ const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
21476
+ return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
21477
+ }
21413
21478
  default: return false;
21414
21479
  }
21415
21480
  };
@@ -22373,7 +22438,8 @@ const SECOND_INDEX_METHODS = new Set([
22373
22438
  "some"
22374
22439
  ]);
22375
22440
  const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
22376
- const isPositionallyStableIterationReceiver = (receiver) => {
22441
+ const isPositionallyStableIterationReceiver = (receiverNode) => {
22442
+ const receiver = stripParenExpression(receiverNode);
22377
22443
  if (isAllLiteralArrayExpression(receiver)) return true;
22378
22444
  if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
22379
22445
  const only = receiver.elements[0];
@@ -22384,17 +22450,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
22384
22450
  }
22385
22451
  if (!isNodeOfType(receiver, "CallExpression")) return false;
22386
22452
  const callee = receiver.callee;
22387
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from" && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22453
+ if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
22388
22454
  if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
22389
22455
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
22390
22456
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
22391
22457
  return false;
22392
22458
  };
22393
- const isArrayFromMapperCallback = (parentCall, callback) => {
22394
- if (parentCall.arguments[1] !== callback) return false;
22395
- const callee = parentCall.callee;
22396
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
22397
- };
22459
+ const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
22398
22460
  const isArrayFromSourcePositionallyStable = (source) => {
22399
22461
  if (isNodeOfType(source, "ObjectExpression")) {
22400
22462
  for (const property of source.properties ?? []) {
@@ -22453,18 +22515,12 @@ const expressionUsesIndex = (expression, paramName) => {
22453
22515
  return false;
22454
22516
  }
22455
22517
  if (isNodeOfType(expression, "CallExpression")) {
22456
- if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
22518
+ if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
22457
22519
  if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
22458
22520
  }
22459
22521
  return false;
22460
22522
  };
22461
- const isReactCloneElement = (callExpression) => {
22462
- const callee = callExpression.callee;
22463
- if (!isNodeOfType(callee, "MemberExpression")) return false;
22464
- if (!isNodeOfType(callee.property, "Identifier")) return false;
22465
- if (callee.property.name !== "cloneElement") return false;
22466
- return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
22467
- };
22523
+ const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
22468
22524
  const noArrayIndexKey = defineRule({
22469
22525
  id: "no-array-index-key",
22470
22526
  title: "Array index used as a key",
@@ -23188,7 +23244,7 @@ const isAsyncFunctionLike = (node) => {
23188
23244
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
23189
23245
  return false;
23190
23246
  };
23191
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
23247
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
23192
23248
  "forEach",
23193
23249
  "map",
23194
23250
  "filter",
@@ -23209,30 +23265,51 @@ const runsOnEffectDispatch = (functionNode) => {
23209
23265
  if (parent.callee === functionNode) return true;
23210
23266
  if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
23211
23267
  const callee = parent.callee;
23212
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
23268
+ return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
23213
23269
  };
23214
23270
  const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
23215
- const isGuardWithTerminatingBranch = (statement) => {
23216
- if (!isNodeOfType(statement, "IfStatement")) return null;
23217
- if (statement.alternate) return null;
23218
- const consequent = statement.consequent;
23219
- if (isTerminatingStatement(consequent)) return consequent;
23220
- if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
23221
- return null;
23222
- };
23223
- const countStatementSequenceSetStateCalls = (statements, context) => {
23271
+ const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
23272
+ const analyzeStatementSequence = (statements, context) => {
23224
23273
  let fallThroughCount = 0;
23225
- let maxTerminatingPathCount = 0;
23274
+ let maxTerminatedCount = 0;
23226
23275
  for (const statement of statements) {
23227
- const guardBranch = isGuardWithTerminatingBranch(statement);
23228
- if (guardBranch) {
23229
- maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
23276
+ if (isNodeOfType(statement, "IfStatement")) {
23277
+ fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
23278
+ const thenSummary = analyzeBranchStatements(statement.consequent, context);
23279
+ const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
23280
+ fallThroughCount: 0,
23281
+ maxTerminatedCount: 0,
23282
+ doAllPathsTerminate: false
23283
+ };
23284
+ maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
23285
+ if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
23286
+ fallThroughCount: 0,
23287
+ maxTerminatedCount,
23288
+ doAllPathsTerminate: true
23289
+ };
23290
+ const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
23291
+ fallThroughCount += Math.max(...fallThroughBranchCounts);
23230
23292
  continue;
23231
23293
  }
23232
- if (isTerminatingStatement(statement)) break;
23294
+ if (isTerminatingStatement(statement)) {
23295
+ const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
23296
+ return {
23297
+ fallThroughCount: 0,
23298
+ maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
23299
+ doAllPathsTerminate: true
23300
+ };
23301
+ }
23233
23302
  fallThroughCount += countMaxPathSetStateCalls(statement, context);
23234
23303
  }
23235
- return Math.max(maxTerminatingPathCount, fallThroughCount);
23304
+ return {
23305
+ fallThroughCount,
23306
+ maxTerminatedCount,
23307
+ doAllPathsTerminate: false
23308
+ };
23309
+ };
23310
+ const countStatementSequenceSetStateCalls = (statements, context) => {
23311
+ const summary = analyzeStatementSequence(statements, context);
23312
+ return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
23236
23313
  };
23237
23314
  const collectLocalHelperFunctions = (root) => {
23238
23315
  const helpersByName = /* @__PURE__ */ new Map();
@@ -23363,7 +23440,9 @@ const isDevOnlyGuardedEffect = (callback) => {
23363
23440
  if (!body || !isNodeOfType(body, "BlockStatement")) return false;
23364
23441
  const firstStatement = (body.body ?? [])[0];
23365
23442
  if (!firstStatement) return false;
23366
- if (!isGuardWithTerminatingBranch(firstStatement)) return false;
23443
+ if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
23444
+ const consequent = firstStatement.consequent;
23445
+ if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
23367
23446
  return mentionsDevEnvFlag(firstStatement.test);
23368
23447
  };
23369
23448
  const noCascadingSetState = defineRule({
@@ -24692,11 +24771,21 @@ const isInitialOnlySetterCall = (callExpr) => {
24692
24771
  return isInitialOnlyPropName(arg.name);
24693
24772
  };
24694
24773
  //#endregion
24774
+ //#region src/plugin/utils/is-no-op-statement.ts
24775
+ const isNoOpStatement = (statement) => {
24776
+ if (isNodeOfType(statement, "EmptyStatement")) return true;
24777
+ if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24778
+ const expression = stripParenExpression(statement.expression);
24779
+ if (isNodeOfType(expression, "Literal")) return true;
24780
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
24781
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
24782
+ return false;
24783
+ };
24784
+ //#endregion
24695
24785
  //#region src/plugin/utils/get-callback-statements.ts
24696
24786
  const getCallbackStatements = (callback) => {
24697
24787
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
24698
- if (isNodeOfType(callback.body, "BlockStatement")) return callback.body.body ?? [];
24699
- return callback.body ? [callback.body] : [];
24788
+ return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
24700
24789
  };
24701
24790
  //#endregion
24702
24791
  //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
@@ -24738,6 +24827,7 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24738
24827
  const flattenGuardedStatements = (statements) => {
24739
24828
  const flattened = [];
24740
24829
  for (const statement of statements) {
24830
+ if (isNoOpStatement(statement)) continue;
24741
24831
  if (isNodeOfType(statement, "ExpressionStatement")) {
24742
24832
  flattened.push(statement);
24743
24833
  continue;
@@ -25422,7 +25512,7 @@ const noDidMountSetState = defineRule({
25422
25512
  const { mode } = resolveSettings$20(context.settings);
25423
25513
  return { CallExpression(node) {
25424
25514
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25425
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25515
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25426
25516
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25427
25517
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25428
25518
  if (isMountFlagArgument(node.arguments?.[0])) return;
@@ -25547,7 +25637,7 @@ const noDidUpdateSetState = defineRule({
25547
25637
  const { mode } = resolveSettings$19(context.settings);
25548
25638
  return { CallExpression(node) {
25549
25639
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
25550
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
25640
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
25551
25641
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
25552
25642
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25553
25643
  if (isInsideDiffGuard(node)) return;
@@ -25868,9 +25958,10 @@ const noDocumentStartViewTransition = defineRule({
25868
25958
  create: (context) => ({ CallExpression(node) {
25869
25959
  const callee = node.callee;
25870
25960
  if (!isNodeOfType(callee, "MemberExpression")) return;
25871
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
25961
+ const receiver = stripParenExpression(callee.object);
25962
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25872
25963
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
25873
- if (context.scopes.symbolFor(callee.object) !== null) return;
25964
+ if (context.scopes.symbolFor(receiver) !== null) return;
25874
25965
  if (!importsReactViewTransition(node)) return;
25875
25966
  context.report({
25876
25967
  node,
@@ -25890,7 +25981,8 @@ const noDocumentWrite = defineRule({
25890
25981
  create: (context) => ({ CallExpression(node) {
25891
25982
  const callee = node.callee;
25892
25983
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
25893
- if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
25984
+ const receiver = stripParenExpression(callee.object);
25985
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
25894
25986
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
25895
25987
  context.report({
25896
25988
  node,
@@ -26587,6 +26679,7 @@ const noEffectEventInDeps = defineRule({
26587
26679
  const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26588
26680
  if (calleeSymbol && calleeSymbol.kind !== "import") return;
26589
26681
  }
26682
+ if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
26590
26683
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26591
26684
  } });
26592
26685
  return {
@@ -29166,7 +29259,7 @@ const noIsMounted = defineRule({
29166
29259
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
29167
29260
  create: (context) => ({ CallExpression(node) {
29168
29261
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
29169
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
29262
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
29170
29263
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
29171
29264
  if (!getParentComponent(node)) return;
29172
29265
  context.report({
@@ -29181,7 +29274,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
29181
29274
  const isJsonMethodCall = (node, method) => {
29182
29275
  if (!isNodeOfType(node, "CallExpression")) return false;
29183
29276
  const callee = node.callee;
29184
- return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
29277
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
29278
+ const receiver = stripParenExpression(callee.object);
29279
+ return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
29185
29280
  };
29186
29281
  const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
29187
29282
  const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
@@ -29592,6 +29687,378 @@ const noLegacyContextApi = defineRule({
29592
29687
  }
29593
29688
  });
29594
29689
  //#endregion
29690
+ //#region src/plugin/utils/executes-during-render.ts
29691
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29692
+ "map",
29693
+ "filter",
29694
+ "forEach",
29695
+ "flatMap",
29696
+ "reduce",
29697
+ "reduceRight",
29698
+ "some",
29699
+ "every",
29700
+ "find",
29701
+ "findIndex",
29702
+ "findLast",
29703
+ "findLastIndex",
29704
+ "sort",
29705
+ "toSorted"
29706
+ ]);
29707
+ const executesDuringRender = (functionNode) => {
29708
+ const parent = functionNode.parent;
29709
+ if (!isNodeOfType(parent, "CallExpression")) return false;
29710
+ if (parent.callee === functionNode) return true;
29711
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
29712
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
29713
+ };
29714
+ //#endregion
29715
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29716
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
29717
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29718
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29719
+ return false;
29720
+ };
29721
+ //#endregion
29722
+ //#region src/plugin/utils/find-declarator-for-binding.ts
29723
+ const findDeclaratorForBinding = (bindingIdentifier) => {
29724
+ let ancestor = bindingIdentifier.parent;
29725
+ while (ancestor) {
29726
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29727
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29728
+ ancestor = ancestor.parent ?? null;
29729
+ }
29730
+ return null;
29731
+ };
29732
+ //#endregion
29733
+ //#region src/plugin/utils/references-falsy-initial-state.ts
29734
+ const isFalsyLiteral = (node) => {
29735
+ if (!node) return true;
29736
+ if (isNodeOfType(node, "Literal")) return !node.value;
29737
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
29738
+ };
29739
+ const isFalsyInitialStateBinding = (identifier) => {
29740
+ if (!isNodeOfType(identifier, "Identifier")) return false;
29741
+ const binding = findVariableInitializer(identifier, identifier.name);
29742
+ if (!binding) return false;
29743
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29744
+ if (!declarator?.init) return false;
29745
+ const init = stripParenExpression(declarator.init);
29746
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
29747
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
29748
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
29749
+ return isFalsyLiteral(init.arguments?.[0]);
29750
+ };
29751
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
29752
+ //#endregion
29753
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29754
+ const isGatedByFalsyInitialState = (node) => {
29755
+ let cursor = node;
29756
+ let parent = node.parent;
29757
+ while (parent) {
29758
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
29759
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29760
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
29761
+ cursor = parent;
29762
+ parent = parent.parent ?? null;
29763
+ }
29764
+ return false;
29765
+ };
29766
+ //#endregion
29767
+ //#region src/plugin/utils/references-client-only-flag.ts
29768
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
29769
+ const referencesClientOnlyFlag = (expression) => {
29770
+ const unwrapped = stripParenExpression(expression);
29771
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
29772
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
29773
+ const property = unwrapped.property;
29774
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
29775
+ }
29776
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
29777
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
29778
+ return false;
29779
+ };
29780
+ //#endregion
29781
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
29782
+ const isInsideClientOnlyGuard = (node) => {
29783
+ let cursor = node;
29784
+ let parent = node.parent;
29785
+ while (parent) {
29786
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
29787
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
29788
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
29789
+ cursor = parent;
29790
+ parent = parent.parent ?? null;
29791
+ }
29792
+ return false;
29793
+ };
29794
+ //#endregion
29795
+ //#region src/plugin/rules/performance/no-locale-format-in-render.ts
29796
+ const LOCALE_FORMAT_METHOD_NAMES = new Set([
29797
+ "toLocaleString",
29798
+ "toLocaleDateString",
29799
+ "toLocaleTimeString"
29800
+ ]);
29801
+ const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
29802
+ const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
29803
+ const INTL_FORMAT_METHOD_NAMES = new Set([
29804
+ "format",
29805
+ "formatToParts",
29806
+ "formatRange"
29807
+ ]);
29808
+ const isProvableDateExpression = (expression) => {
29809
+ if (!expression) return false;
29810
+ const unwrapped = stripParenExpression(expression);
29811
+ return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
29812
+ };
29813
+ const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
29814
+ const receiverNameLooksDateFlavored = (expression) => {
29815
+ if (!expression) return false;
29816
+ const unwrapped = stripParenExpression(expression);
29817
+ if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
29818
+ if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
29819
+ if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
29820
+ return false;
29821
+ };
29822
+ const objectLiteralHasProperty = (objectExpression, propertyName) => {
29823
+ if (!objectExpression) return false;
29824
+ const unwrapped = stripParenExpression(objectExpression);
29825
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
29826
+ for (const property of unwrapped.properties ?? []) {
29827
+ if (!isNodeOfType(property, "Property")) continue;
29828
+ if (property.computed) continue;
29829
+ if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
29830
+ if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
29831
+ }
29832
+ return false;
29833
+ };
29834
+ const hasExplicitLocaleArgument = (argument) => {
29835
+ if (!argument) return false;
29836
+ const unwrapped = stripParenExpression(argument);
29837
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
29838
+ return true;
29839
+ };
29840
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
29841
+ const localeArgument = call.arguments?.[0];
29842
+ if (!hasExplicitLocaleArgument(localeArgument)) return false;
29843
+ const optionsArgument = call.arguments?.[1];
29844
+ if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
29845
+ return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
29846
+ };
29847
+ const matchLocaleMethodCall = (call) => {
29848
+ const callee = call.callee;
29849
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29850
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29851
+ const methodName = callee.property.name;
29852
+ if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
29853
+ const receiverIsProvablyDate = isProvableDateExpression(callee.object);
29854
+ if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
29855
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
29856
+ return {
29857
+ node: call,
29858
+ display: `${methodName}()`
29859
+ };
29860
+ };
29861
+ const getIntlFormatterName = (expression) => {
29862
+ if (!expression) return null;
29863
+ const unwrapped = stripParenExpression(expression);
29864
+ if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
29865
+ const callee = unwrapped.callee;
29866
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29867
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
29868
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29869
+ return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
29870
+ };
29871
+ const isDeterministicIntlConstruction = (construction, formatterName) => {
29872
+ if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
29873
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
29874
+ if (formatterName !== "DateTimeFormat") return true;
29875
+ return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
29876
+ };
29877
+ const matchIntlFormatCall = (call) => {
29878
+ const callee = call.callee;
29879
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
29880
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
29881
+ if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
29882
+ let construction = stripParenExpression(callee.object);
29883
+ if (isNodeOfType(construction, "Identifier")) {
29884
+ const binding = findVariableInitializer(construction, construction.name);
29885
+ construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
29886
+ }
29887
+ if (!construction) return null;
29888
+ const formatterName = getIntlFormatterName(construction);
29889
+ if (!formatterName) return null;
29890
+ if (isDeterministicIntlConstruction(construction, formatterName)) return null;
29891
+ return {
29892
+ node: call,
29893
+ display: `Intl.${formatterName}().${callee.property.name}()`
29894
+ };
29895
+ };
29896
+ const isDeterministicInputDateConstruction = (expression) => {
29897
+ if (!expression) return false;
29898
+ const unwrapped = stripParenExpression(expression);
29899
+ if (!isNodeOfType(unwrapped, "NewExpression")) return false;
29900
+ if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
29901
+ return (unwrapped.arguments?.length ?? 0) > 0;
29902
+ };
29903
+ const matchDateDefaultStringification = (node) => {
29904
+ if (isNodeOfType(node, "CallExpression")) {
29905
+ const callee = node.callee;
29906
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
29907
+ node,
29908
+ display: "Date.prototype.toString()"
29909
+ };
29910
+ if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
29911
+ node,
29912
+ display: "String(new Date(…))"
29913
+ };
29914
+ return null;
29915
+ }
29916
+ if (isNodeOfType(node, "TemplateLiteral")) {
29917
+ for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
29918
+ node: expression,
29919
+ display: "`${new Date(…)}`"
29920
+ };
29921
+ }
29922
+ return null;
29923
+ };
29924
+ const findRenderPhaseComponentOrHook = (node) => {
29925
+ let functionNode = findEnclosingFunction(node);
29926
+ while (functionNode) {
29927
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29928
+ if (!executesDuringRender(functionNode)) return null;
29929
+ functionNode = findEnclosingFunction(functionNode);
29930
+ }
29931
+ return null;
29932
+ };
29933
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29934
+ if (fileHasUseClientDirective) return true;
29935
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
29936
+ if (displayName && isReactHookName(displayName)) return true;
29937
+ let callsHook = false;
29938
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
29939
+ if (callsHook) return false;
29940
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
29941
+ callsHook = true;
29942
+ return false;
29943
+ }
29944
+ });
29945
+ return callsHook;
29946
+ };
29947
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
29948
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
29949
+ if (!isNodeOfType(body, "BlockStatement")) return false;
29950
+ const ancestors = /* @__PURE__ */ new Set();
29951
+ let cursor = node;
29952
+ while (cursor) {
29953
+ ancestors.add(cursor);
29954
+ cursor = cursor.parent ?? null;
29955
+ }
29956
+ for (const statement of body.body ?? []) {
29957
+ if (ancestors.has(statement)) return false;
29958
+ if (!isNodeOfType(statement, "IfStatement")) continue;
29959
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
29960
+ let returnsEarly = false;
29961
+ walkAst(statement.consequent, (child) => {
29962
+ if (isFunctionLike$1(child)) return false;
29963
+ if (isNodeOfType(child, "ReturnStatement")) {
29964
+ returnsEarly = true;
29965
+ return false;
29966
+ }
29967
+ });
29968
+ if (returnsEarly) return true;
29969
+ }
29970
+ return false;
29971
+ };
29972
+ const findEnclosingJsxOpeningElement = (node) => {
29973
+ let cursor = node.parent;
29974
+ while (cursor) {
29975
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
29976
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
29977
+ cursor = cursor.parent ?? null;
29978
+ }
29979
+ return null;
29980
+ };
29981
+ const noLocaleFormatInRender = defineRule({
29982
+ id: "no-locale-format-in-render",
29983
+ title: "Locale/timezone formatting during render",
29984
+ severity: "warn",
29985
+ category: "Correctness",
29986
+ disabledWhen: [
29987
+ "vite",
29988
+ "cra",
29989
+ "expo",
29990
+ "react-native",
29991
+ "unknown"
29992
+ ],
29993
+ recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
29994
+ create: (context) => {
29995
+ if (isTestlikeFilename(context.filename)) return {};
29996
+ if (classifyReactNativeFileTarget(context) === "react-native") return {};
29997
+ let fileHasUseClientDirective = false;
29998
+ let fileIsEmailTemplate = false;
29999
+ const reportedNodes = /* @__PURE__ */ new Set();
30000
+ const reportIfRenderPhase = (match) => {
30001
+ if (reportedNodes.has(match.node)) return;
30002
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
30003
+ if (!componentOrHookNode) return;
30004
+ if (fileIsEmailTemplate) return;
30005
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30006
+ if (isInsideClientOnlyGuard(match.node)) return;
30007
+ if (isGatedByFalsyInitialState(match.node)) return;
30008
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
30009
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
30010
+ if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
30011
+ reportedNodes.add(match.node);
30012
+ context.report({
30013
+ node: match.node,
30014
+ message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30015
+ });
30016
+ };
30017
+ return {
30018
+ Program(node) {
30019
+ fileHasUseClientDirective = hasDirective(node, "use client");
30020
+ fileIsEmailTemplate = hasEmailTemplateImport(node);
30021
+ },
30022
+ CallExpression(node) {
30023
+ const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
30024
+ if (match) reportIfRenderPhase(match);
30025
+ },
30026
+ TemplateLiteral(node) {
30027
+ const match = matchDateDefaultStringification(node);
30028
+ if (match) reportIfRenderPhase(match);
30029
+ },
30030
+ JSXExpressionContainer(node) {
30031
+ const expression = stripParenExpression(node.expression);
30032
+ if (!isNodeOfType(expression, "CallExpression")) return;
30033
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
30034
+ const helperName = expression.callee.name;
30035
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
30036
+ if (!componentOrHookNode) return;
30037
+ const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
30038
+ if (!helperNode || !isFunctionLike$1(helperNode)) return;
30039
+ if (componentOrHookDisplayNameForFunction(helperNode)) return;
30040
+ walkAst(helperNode.body ?? helperNode, (child) => {
30041
+ if (isFunctionLike$1(child)) return false;
30042
+ if (!isNodeOfType(child, "CallExpression")) return;
30043
+ const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
30044
+ if (!match || reportedNodes.has(match.node)) return;
30045
+ if (fileIsEmailTemplate) return;
30046
+ if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
30047
+ if (isInsideClientOnlyGuard(node)) return;
30048
+ if (isGatedByFalsyInitialState(node)) return;
30049
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
30050
+ if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
30051
+ reportedNodes.add(match.node);
30052
+ context.report({
30053
+ node: match.node,
30054
+ message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
30055
+ });
30056
+ });
30057
+ }
30058
+ };
30059
+ }
30060
+ });
30061
+ //#endregion
29595
30062
  //#region src/plugin/rules/design/no-long-transition-duration.ts
29596
30063
  const hasInfiniteIterationCount = (properties) => properties.some((property) => {
29597
30064
  if (getStylePropertyKey(property) !== "animationIterationCount") return false;
@@ -31704,12 +32171,12 @@ const noPassDataToParent = defineRule({
31704
32171
  if (calleeNode === identifier) {
31705
32172
  if (!isDirectParentCallbackRef(analysis, ref)) continue;
31706
32173
  if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
31707
- } else if (isNodeOfType(calleeNode, "MemberExpression") && unwrapChainExpression(calleeNode.object) === identifier) {
32174
+ } else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
31708
32175
  if (!isWholePropsObjectReference(analysis, ref)) continue;
31709
32176
  if (isCustomHookParameter(ref)) continue;
31710
32177
  } else continue;
31711
32178
  const methodName = getCallMethodName(calleeNode);
31712
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32179
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31713
32180
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31714
32181
  if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
31715
32182
  if (isNamespacedApiCallee(calleeNode)) continue;
@@ -31900,7 +32367,7 @@ const noPassLiveStateToParent = defineRule({
31900
32367
  if (isCallResultConsumedAsArgument(callExpr)) continue;
31901
32368
  const calleeNode = callExpr.callee;
31902
32369
  const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
31903
- const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
32370
+ const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
31904
32371
  if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
31905
32372
  if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
31906
32373
  const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
@@ -32434,7 +32901,7 @@ const isAlwaysFreshExpression = (expression) => {
32434
32901
  return `${callee.name}()`;
32435
32902
  }
32436
32903
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
32437
- const receiver = callee.object;
32904
+ const receiver = stripParenExpression(callee.object);
32438
32905
  const property = callee.property;
32439
32906
  if (!isNodeOfType(property, "Identifier")) return null;
32440
32907
  if (isNodeOfType(receiver, "Identifier")) {
@@ -32555,8 +33022,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32555
33022
  if (typeof sourceValue !== "string") return;
32556
33023
  if (handleExtraSource?.(node, context)) return;
32557
33024
  if (sourceValue !== source) return;
33025
+ if (isTypeOnlyImport(node)) return;
32558
33026
  for (const specifier of node.specifiers ?? []) {
32559
33027
  if (isNodeOfType(specifier, "ImportSpecifier")) {
33028
+ if (specifier.importKind === "type") continue;
32560
33029
  const importedName = getImportedName$1(specifier);
32561
33030
  if (!importedName) continue;
32562
33031
  const message = messages.get(importedName);
@@ -32575,8 +33044,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
32575
33044
  MemberExpression(node) {
32576
33045
  if (namespaceBindings.size === 0) return;
32577
33046
  if (node.computed) return;
32578
- if (!isNodeOfType(node.object, "Identifier")) return;
32579
- if (!namespaceBindings.has(node.object.name)) return;
33047
+ const receiver = stripParenExpression(node.object);
33048
+ if (!isNodeOfType(receiver, "Identifier")) return;
33049
+ if (!namespaceBindings.has(receiver.name)) return;
32580
33050
  if (!isNodeOfType(node.property, "Identifier")) return;
32581
33051
  const message = messages.get(node.property.name);
32582
33052
  if (message) context.report({
@@ -32641,7 +33111,7 @@ const noReactDomDeprecatedApis = defineRule({
32641
33111
  });
32642
33112
  //#endregion
32643
33113
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
32644
- const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
33114
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
32645
33115
  const isVendoredShadcnUiFilename = (rawFilename) => {
32646
33116
  if (!rawFilename) return false;
32647
33117
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32679,7 +33149,7 @@ const noReact19DeprecatedApis = defineRule({
32679
33149
  requires: ["react:19"],
32680
33150
  tags: ["test-noise", "migration-hint"],
32681
33151
  severity: "warn",
32682
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
33152
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32683
33153
  create: (context) => {
32684
33154
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32685
33155
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -33003,34 +33473,11 @@ const noRedundantShouldComponentUpdate = defineRule({
33003
33473
  });
33004
33474
  //#endregion
33005
33475
  //#region src/plugin/rules/architecture/no-render-in-render.ts
33006
- const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
33007
- if (!symbol || visitedSymbols.has(symbol)) return false;
33008
- visitedSymbols.add(symbol);
33009
- if (isComponentParameterSymbol(symbol)) return true;
33010
- if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
33011
- const source = symbol.initializer;
33012
- if (!source) return false;
33013
- return initializerRootsInProps(source, scopes, visitedSymbols);
33014
- };
33015
- const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
33016
- if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
33017
- if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
33018
- return rootsInProps(node, scopes, visitedSymbols);
33019
- };
33020
- const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
33021
- let current = node;
33022
- while (isNodeOfType(current, "MemberExpression")) {
33023
- if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
33024
- current = current.object;
33025
- }
33026
- if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
33027
- return false;
33028
- };
33029
33476
  const isInsideComponentContext = (node) => {
33030
33477
  let cursor = node.parent;
33031
33478
  while (cursor) {
33032
- if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
33033
33479
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33480
+ if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
33034
33481
  cursor = cursor.parent ?? null;
33035
33482
  }
33036
33483
  return false;
@@ -33040,24 +33487,28 @@ const functionBodyOf = (node) => {
33040
33487
  if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
33041
33488
  return null;
33042
33489
  };
33490
+ const isHookCallee = (callee) => {
33491
+ if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
33492
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
33493
+ return false;
33494
+ };
33043
33495
  const containsHookCall = (body) => {
33044
33496
  let found = false;
33045
33497
  walkAst(body, (child) => {
33046
- if (found) return;
33498
+ if (found) return false;
33499
+ if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
33047
33500
  if (!isNodeOfType(child, "CallExpression")) return;
33048
- const name = getCalleeName$2(child);
33049
- if (name && isReactHookName(name)) found = true;
33501
+ if (isHookCallee(child.callee)) found = true;
33050
33502
  });
33051
33503
  return found;
33052
33504
  };
33053
- const isModuleScopeHookFreeHelper = (symbol) => {
33505
+ const isHookCallingRenderHelper = (symbol) => {
33054
33506
  if (!symbol) return false;
33055
33507
  const declaration = symbol.declarationNode;
33056
33508
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
33057
33509
  const body = functionBodyOf(declaration);
33058
33510
  if (!body) return false;
33059
- if (findEnclosingFunction(declaration) !== null) return false;
33060
- return !containsHookCall(body);
33511
+ return containsHookCall(body);
33061
33512
  };
33062
33513
  const noRenderInRender = defineRule({
33063
33514
  id: "no-render-in-render",
@@ -33066,20 +33517,13 @@ const noRenderInRender = defineRule({
33066
33517
  tags: ["test-noise"],
33067
33518
  recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
33068
33519
  create: (context) => ({ JSXExpressionContainer(node) {
33069
- const expression = node.expression;
33520
+ const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
33070
33521
  if (!isNodeOfType(expression, "CallExpression")) return;
33071
- let calleeName = null;
33072
- if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
33073
- else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
33074
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33522
+ if (!isNodeOfType(expression.callee, "Identifier")) return;
33523
+ const calleeName = expression.callee.name;
33524
+ if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33075
33525
  if (!isInsideComponentContext(node)) return;
33076
- if (isNodeOfType(expression.callee, "Identifier")) {
33077
- const calleeSymbol = context.scopes.symbolFor(expression.callee);
33078
- if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
33079
- if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
33080
- } else if (isNodeOfType(expression.callee, "MemberExpression")) {
33081
- if (rootsInProps(expression.callee.object, context.scopes)) return;
33082
- }
33526
+ if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
33083
33527
  context.report({
33084
33528
  node: expression,
33085
33529
  message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
@@ -33140,13 +33584,7 @@ const noRenderPropChildren = defineRule({
33140
33584
  //#endregion
33141
33585
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
33142
33586
  const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
33143
- const isReactDomRenderCall = (node) => {
33144
- if (!isNodeOfType(node.callee, "MemberExpression")) return false;
33145
- if (!isNodeOfType(node.callee.object, "Identifier")) return false;
33146
- if (node.callee.object.name !== "ReactDOM") return false;
33147
- if (!isNodeOfType(node.callee.property, "Identifier")) return false;
33148
- return node.callee.property.name === "render";
33149
- };
33587
+ const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
33150
33588
  const isUsedAsReturnValue = (parent) => {
33151
33589
  if (!parent) return false;
33152
33590
  if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
@@ -33982,7 +34420,7 @@ const noSetState = defineRule({
33982
34420
  category: "Architecture",
33983
34421
  create: (context) => ({ CallExpression(node) {
33984
34422
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
33985
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
34423
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
33986
34424
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
33987
34425
  if (!getParentComponent(node)) return;
33988
34426
  context.report({
@@ -36428,6 +36866,36 @@ const isTriviallyCheapExpression = (node) => {
36428
36866
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36429
36867
  return true;
36430
36868
  };
36869
+ const isTrivialContainerLiteral = (node) => {
36870
+ if (!node) return false;
36871
+ const innerExpression = stripParenExpression(node);
36872
+ if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36873
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36874
+ return false;
36875
+ };
36876
+ const isNonEscapingRead = (identifier) => {
36877
+ const readRoot = findTransparentExpressionRoot(identifier);
36878
+ const memberNode = readRoot.parent;
36879
+ if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
36880
+ const memberUse = findTransparentExpressionRoot(memberNode);
36881
+ const memberUseParent = memberUse.parent;
36882
+ if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
36883
+ if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
36884
+ if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
36885
+ return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
36886
+ };
36887
+ const isMemoIdentityUnused = (memoCallNode, scopes) => {
36888
+ const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
36889
+ const parentNode = memoUsageRoot.parent;
36890
+ if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36891
+ if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
36892
+ const bindingTarget = parentNode.id;
36893
+ if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36894
+ if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36895
+ const symbol = scopes.symbolFor(bindingTarget);
36896
+ if (!symbol) return false;
36897
+ return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36898
+ };
36431
36899
  const noUsememoSimpleExpression = defineRule({
36432
36900
  id: "no-usememo-simple-expression",
36433
36901
  title: "useMemo on a cheap value",
@@ -36449,9 +36917,17 @@ const noUsememoSimpleExpression = defineRule({
36449
36917
  let returnExpression = null;
36450
36918
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36451
36919
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36452
- if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
36920
+ if (!returnExpression) return;
36921
+ if (isTriviallyCheapExpression(returnExpression)) {
36922
+ context.report({
36923
+ node,
36924
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36925
+ });
36926
+ return;
36927
+ }
36928
+ if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36453
36929
  node,
36454
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36930
+ message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36455
36931
  });
36456
36932
  } })
36457
36933
  });
@@ -36557,7 +37033,7 @@ const noWillUpdateSetState = defineRule({
36557
37033
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
36558
37034
  return { CallExpression(node) {
36559
37035
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
36560
- if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
37036
+ if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
36561
37037
  if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
36562
37038
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
36563
37039
  context.report({
@@ -36913,7 +37389,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
36913
37389
  continue;
36914
37390
  }
36915
37391
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36916
- if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
37392
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
36917
37393
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36918
37394
  }
36919
37395
  return false;
@@ -37021,18 +37497,22 @@ const onlyExportComponents = defineRule({
37021
37497
  customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
37022
37498
  allowExportNames: new Set(settings.allowExportNames),
37023
37499
  allowConstantExport: settings.allowConstantExport,
37024
- localComponentNames
37500
+ localComponentNames,
37501
+ scopes: context.scopes
37025
37502
  };
37026
37503
  for (const child of componentCandidates) {
37027
37504
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
37028
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37505
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
37029
37506
  }
37030
37507
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
37031
37508
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37032
37509
  }
37033
37510
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
37034
37511
  const initializer = child.init;
37035
- if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37512
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
37513
+ const expression = initializer ? skipTsExpression(initializer) : null;
37514
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
37515
+ }
37036
37516
  }
37037
37517
  }
37038
37518
  const exports = [];
@@ -38697,11 +39177,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
38697
39177
  }
38698
39178
  return null;
38699
39179
  };
39180
+ const isTrivialLiteralExpression = (expression) => {
39181
+ if (isNodeOfType(expression, "Literal")) return true;
39182
+ if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
39183
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
39184
+ if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
39185
+ return false;
39186
+ };
38700
39187
  const getSingleSetterCallFromHandler = (handler) => {
38701
39188
  const handlerStatements = getCallbackStatements(handler);
38702
39189
  if (handlerStatements.length !== 1) return null;
38703
39190
  const onlyStatement = handlerStatements[0];
38704
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
39191
+ let expression = onlyStatement;
39192
+ if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
39193
+ if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
39194
+ if (!expression) return null;
39195
+ expression = stripParenExpression(expression);
38705
39196
  if (!isNodeOfType(expression, "CallExpression")) return null;
38706
39197
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
38707
39198
  if (!isSetterIdentifier(expression.callee.name)) return null;
@@ -38711,6 +39202,106 @@ const getSingleSetterCallFromHandler = (handler) => {
38711
39202
  setterArgument: expression.arguments[0]
38712
39203
  };
38713
39204
  };
39205
+ const isListenerCollectionInitializer = (init) => {
39206
+ if (!init) return false;
39207
+ if (isNodeOfType(init, "ArrayExpression")) return true;
39208
+ return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
39209
+ };
39210
+ const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
39211
+ if (!isFunctionLike$1(functionNode)) return false;
39212
+ const firstParam = functionNode.params?.[0];
39213
+ if (!isNodeOfType(firstParam, "Identifier")) return false;
39214
+ const listenerParamName = firstParam.name;
39215
+ let registersListener = false;
39216
+ walkAst(functionNode.body, (child) => {
39217
+ if (registersListener) return false;
39218
+ if (!isNodeOfType(child, "CallExpression")) return;
39219
+ if (!isNodeOfType(child.callee, "MemberExpression")) return;
39220
+ if (!isNodeOfType(child.callee.object, "Identifier")) return;
39221
+ if (!listenerCollectionNames.has(child.callee.object.name)) return;
39222
+ if (!isNodeOfType(child.callee.property, "Identifier")) return;
39223
+ if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
39224
+ const registeredArgument = child.arguments?.[0];
39225
+ if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
39226
+ });
39227
+ return registersListener;
39228
+ };
39229
+ const buildModuleScopeStoreIndex = (programRoot) => {
39230
+ const mutableBindingNames = /* @__PURE__ */ new Set();
39231
+ const listenerCollectionNames = /* @__PURE__ */ new Set();
39232
+ const moduleFunctionsByName = /* @__PURE__ */ new Map();
39233
+ if (!isNodeOfType(programRoot, "Program")) return {
39234
+ mutableBindingNames,
39235
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39236
+ };
39237
+ for (const statement of programRoot.body ?? []) {
39238
+ const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
39239
+ if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
39240
+ moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
39241
+ continue;
39242
+ }
39243
+ if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
39244
+ for (const declarator of unwrapped.declarations ?? []) {
39245
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
39246
+ const init = declarator.init ?? null;
39247
+ if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
39248
+ mutableBindingNames.add(declarator.id.name);
39249
+ continue;
39250
+ }
39251
+ if (isListenerCollectionInitializer(init)) {
39252
+ listenerCollectionNames.add(declarator.id.name);
39253
+ continue;
39254
+ }
39255
+ if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
39256
+ }
39257
+ }
39258
+ const subscribeFunctionNames = /* @__PURE__ */ new Set();
39259
+ for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
39260
+ return {
39261
+ mutableBindingNames,
39262
+ subscribeFunctionNames
39263
+ };
39264
+ };
39265
+ const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
39266
+ if (!isNodeOfType(useStateCall, "CallExpression")) return null;
39267
+ let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
39268
+ if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
39269
+ if (!isNodeOfType(initialArgument, "Identifier")) return null;
39270
+ if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
39271
+ const binding = findVariableInitializer(initialArgument, initialArgument.name);
39272
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
39273
+ return initialArgument.name;
39274
+ };
39275
+ const argumentForwardsSetter = (argument, setterName) => {
39276
+ if (!argument) return false;
39277
+ const unwrapped = stripParenExpression(argument);
39278
+ if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
39279
+ if (!isFunctionLike$1(unwrapped)) return false;
39280
+ let callsSetter = false;
39281
+ walkAst(unwrapped.body, (child) => {
39282
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
39283
+ callsSetter = true;
39284
+ return false;
39285
+ }
39286
+ });
39287
+ return callsSetter;
39288
+ };
39289
+ const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
39290
+ let matchedCall = null;
39291
+ walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
39292
+ if (matchedCall) return false;
39293
+ if (!isNodeOfType(child, "CallExpression")) return;
39294
+ if (!isNodeOfType(child.callee, "Identifier")) return;
39295
+ if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
39296
+ const binding = findVariableInitializer(child.callee, child.callee.name);
39297
+ if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
39298
+ for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
39299
+ matchedCall = child;
39300
+ return false;
39301
+ }
39302
+ });
39303
+ return matchedCall;
39304
+ };
38714
39305
  const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
38715
39306
  const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
38716
39307
  if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
@@ -38727,6 +39318,16 @@ const preferUseSyncExternalStore = defineRule({
38727
39318
  severity: "warn",
38728
39319
  recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
38729
39320
  create: (context) => {
39321
+ let cachedStoreIndex = null;
39322
+ const storeIndexFor = (node) => {
39323
+ if (cachedStoreIndex) return cachedStoreIndex;
39324
+ const programRoot = findProgramRoot(node);
39325
+ cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
39326
+ mutableBindingNames: /* @__PURE__ */ new Set(),
39327
+ subscribeFunctionNames: /* @__PURE__ */ new Set()
39328
+ };
39329
+ return cachedStoreIndex;
39330
+ };
38730
39331
  const checkComponent = (componentBody) => {
38731
39332
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
38732
39333
  const useStateBindings = collectUseStateBindings(componentBody);
@@ -38764,6 +39365,7 @@ const preferUseSyncExternalStore = defineRule({
38764
39365
  const useStateInitializer = useStateInitializerByValueName.get(valueName);
38765
39366
  if (!useStateInitializer) continue;
38766
39367
  if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
39368
+ if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
38767
39369
  if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
38768
39370
  const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
38769
39371
  context.report({
@@ -38771,14 +39373,47 @@ const preferUseSyncExternalStore = defineRule({
38771
39373
  message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
38772
39374
  });
38773
39375
  }
39376
+ checkModuleStoreShape(componentBody, useStateBindings);
39377
+ };
39378
+ const checkModuleStoreShape = (componentBody, useStateBindings) => {
39379
+ const storeIndex = storeIndexFor(componentBody);
39380
+ if (storeIndex.mutableBindingNames.size === 0) return;
39381
+ if (storeIndex.subscribeFunctionNames.size === 0) return;
39382
+ const snapshotBindings = useStateBindings.map((binding) => ({
39383
+ binding,
39384
+ storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
39385
+ })).filter((candidate) => candidate.storeName !== null);
39386
+ if (snapshotBindings.length === 0) return;
39387
+ const reportedDeclarators = /* @__PURE__ */ new Set();
39388
+ for (const effectCall of findUseEffectsInComponent(componentBody)) {
39389
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
39390
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
39391
+ const depsNode = effectCall.arguments[1];
39392
+ if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
39393
+ if ((depsNode.elements?.length ?? 0) !== 0) continue;
39394
+ const callback = getEffectCallback(effectCall);
39395
+ if (!callback || !isFunctionLike$1(callback)) continue;
39396
+ for (const { binding, storeName } of snapshotBindings) {
39397
+ if (reportedDeclarators.has(binding.declarator)) continue;
39398
+ if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
39399
+ reportedDeclarators.add(binding.declarator);
39400
+ context.report({
39401
+ node: binding.declarator,
39402
+ message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
39403
+ });
39404
+ }
39405
+ }
38774
39406
  };
38775
39407
  return {
38776
39408
  FunctionDeclaration(node) {
38777
- if (!node.id?.name || !isUppercaseName(node.id.name)) return;
39409
+ const functionName = node.id?.name;
39410
+ if (!functionName) return;
39411
+ if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
38778
39412
  checkComponent(node.body);
38779
39413
  },
38780
39414
  VariableDeclarator(node) {
38781
- if (!isComponentAssignment(node)) return;
39415
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
39416
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
38782
39417
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
38783
39418
  checkComponent(node.init.body);
38784
39419
  }
@@ -39388,7 +40023,7 @@ const queryNoVoidQueryFn = defineRule({
39388
40023
  if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
39389
40024
  const body = queryFnValue.body;
39390
40025
  if (!isNodeOfType(body, "BlockStatement")) return;
39391
- if ((body.body ?? []).length === 0) context.report({
40026
+ if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
39392
40027
  node: queryFnProperty,
39393
40028
  message: "This empty queryFn caches undefined, so the component never gets data."
39394
40029
  });
@@ -39895,17 +40530,6 @@ const renderingHoistJsx = defineRule({
39895
40530
  }
39896
40531
  });
39897
40532
  //#endregion
39898
- //#region src/plugin/utils/find-declarator-for-binding.ts
39899
- const findDeclaratorForBinding = (bindingIdentifier) => {
39900
- let ancestor = bindingIdentifier.parent;
39901
- while (ancestor) {
39902
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
39903
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
39904
- ancestor = ancestor.parent ?? null;
39905
- }
39906
- return null;
39907
- };
39908
- //#endregion
39909
40533
  //#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
39910
40534
  const NONDETERMINISTIC_RENDER_PATTERNS = [
39911
40535
  {
@@ -39914,19 +40538,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
39914
40538
  },
39915
40539
  {
39916
40540
  display: "Date.now()",
39917
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Date" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40541
+ matches: (node) => isGlobalMethodCall(node, "Date", "now")
39918
40542
  },
39919
40543
  {
39920
40544
  display: "Math.random()",
39921
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "Math" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "random"
40545
+ matches: (node) => isGlobalMethodCall(node, "Math", "random")
39922
40546
  },
39923
40547
  {
39924
40548
  display: "performance.now()",
39925
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "performance" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "now"
40549
+ matches: (node) => isGlobalMethodCall(node, "performance", "now")
39926
40550
  },
39927
40551
  {
39928
40552
  display: "crypto.randomUUID()",
39929
- matches: (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.object, "Identifier") && node.callee.object.name === "crypto" && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "randomUUID"
40553
+ matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
39930
40554
  }
39931
40555
  ];
39932
40556
  const findOpeningElementOfChild = (jsxNode) => {
@@ -39938,54 +40562,6 @@ const findOpeningElementOfChild = (jsxNode) => {
39938
40562
  }
39939
40563
  return null;
39940
40564
  };
39941
- const executesDuringRender = (functionNode) => {
39942
- const parent = functionNode.parent;
39943
- if (!isNodeOfType(parent, "CallExpression")) return false;
39944
- if (parent.callee === functionNode) return true;
39945
- return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
39946
- };
39947
- const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
39948
- const referencesClientOnlyFlag = (expression) => {
39949
- const unwrapped = stripParenExpression(expression);
39950
- if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
39951
- if (isNodeOfType(unwrapped, "MemberExpression")) {
39952
- const property = unwrapped.property;
39953
- return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
39954
- }
39955
- if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
39956
- if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
39957
- return false;
39958
- };
39959
- const isFalsyLiteral = (node) => {
39960
- if (!node) return true;
39961
- if (isNodeOfType(node, "Literal")) return !node.value;
39962
- return isNodeOfType(node, "Identifier") && node.name === "undefined";
39963
- };
39964
- const isFalsyInitialStateBinding = (identifier) => {
39965
- if (!isNodeOfType(identifier, "Identifier")) return false;
39966
- const binding = findVariableInitializer(identifier, identifier.name);
39967
- if (!binding) return false;
39968
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
39969
- if (!declarator?.init) return false;
39970
- const init = stripParenExpression(declarator.init);
39971
- if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
39972
- if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
39973
- if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
39974
- return isFalsyLiteral(init.arguments?.[0]);
39975
- };
39976
- const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
39977
- const isGatedByFalsyInitialState = (node) => {
39978
- let cursor = node;
39979
- let parent = node.parent;
39980
- while (parent) {
39981
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
39982
- if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39983
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
39984
- cursor = parent;
39985
- parent = parent.parent ?? null;
39986
- }
39987
- return false;
39988
- };
39989
40565
  const isYearOnlyDateRead = (dateNode) => {
39990
40566
  const member = dateNode.parent;
39991
40567
  if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
@@ -40009,23 +40585,6 @@ const isInsideMotionTransitionAttribute = (node) => {
40009
40585
  }
40010
40586
  return false;
40011
40587
  };
40012
- const isInsideClientOnlyGuard = (node) => {
40013
- let cursor = node;
40014
- let parent = node.parent;
40015
- while (parent) {
40016
- if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
40017
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
40018
- if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
40019
- cursor = parent;
40020
- parent = parent.parent ?? null;
40021
- }
40022
- return false;
40023
- };
40024
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
40025
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
40026
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
40027
- return false;
40028
- };
40029
40588
  const renderingHydrationMismatchTime = defineRule({
40030
40589
  id: "rendering-hydration-mismatch-time",
40031
40590
  title: "Time or random value in JSX",
@@ -40073,6 +40632,35 @@ const renderingHydrationMismatchTime = defineRule({
40073
40632
  }
40074
40633
  });
40075
40634
  //#endregion
40635
+ //#region src/plugin/utils/contains-locale-environment-read.ts
40636
+ const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
40637
+ "toLocaleString",
40638
+ "toLocaleDateString",
40639
+ "toLocaleTimeString",
40640
+ "getTimezoneOffset"
40641
+ ]);
40642
+ const containsLocaleEnvironmentRead = (expression) => {
40643
+ let readsLocaleEnvironment = false;
40644
+ walkAst(expression, (child) => {
40645
+ if (readsLocaleEnvironment) return false;
40646
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
40647
+ if (child.object.name === "Intl") {
40648
+ readsLocaleEnvironment = true;
40649
+ return false;
40650
+ }
40651
+ if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
40652
+ readsLocaleEnvironment = true;
40653
+ return false;
40654
+ }
40655
+ }
40656
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
40657
+ readsLocaleEnvironment = true;
40658
+ return false;
40659
+ }
40660
+ });
40661
+ return readsLocaleEnvironment;
40662
+ };
40663
+ //#endregion
40076
40664
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
40077
40665
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
40078
40666
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -40138,14 +40726,15 @@ const renderingHydrationNoFlicker = defineRule({
40138
40726
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
40139
40727
  const callback = getEffectCallback(node);
40140
40728
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
40141
- const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
40142
- if (!bodyStatements || bodyStatements.length !== 1) return;
40729
+ const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
40730
+ if (bodyStatements.length !== 1) return;
40143
40731
  const soleStatement = bodyStatements[0];
40144
40732
  if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
40145
40733
  const expression = soleStatement.expression;
40146
40734
  if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
40147
40735
  if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
40148
40736
  if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
40737
+ if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
40149
40738
  context.report({
40150
40739
  node,
40151
40740
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
@@ -40964,6 +41553,9 @@ const rerenderFunctionalSetstate = defineRule({
40964
41553
  } })
40965
41554
  });
40966
41555
  //#endregion
41556
+ //#region src/plugin/utils/is-trivial-built-in-construction.ts
41557
+ const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
41558
+ //#endregion
40967
41559
  //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
40968
41560
  const rerenderLazyRefInit = defineRule({
40969
41561
  id: "rerender-lazy-ref-init",
@@ -40974,7 +41566,7 @@ const rerenderLazyRefInit = defineRule({
40974
41566
  recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
40975
41567
  create: (context) => ({ CallExpression(node) {
40976
41568
  if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
40977
- const initializer = node.arguments[0];
41569
+ const initializer = stripParenExpression(node.arguments[0]);
40978
41570
  const isPlainCall = isNodeOfType(initializer, "CallExpression");
40979
41571
  const isNewCall = isNodeOfType(initializer, "NewExpression");
40980
41572
  if (!isPlainCall && !isNewCall) return;
@@ -40982,7 +41574,7 @@ const rerenderLazyRefInit = defineRule({
40982
41574
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40983
41575
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40984
41576
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40985
- if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41577
+ if (isTrivialBuiltInConstruction(initializer)) return;
40986
41578
  if (isPlainCall && isReactHookName(calleeName)) return;
40987
41579
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40988
41580
  context.report({
@@ -41018,11 +41610,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
41018
41610
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
41019
41611
  const findEagerInitializerCall = (expression, depth = 0) => {
41020
41612
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
41021
- if (isNodeOfType(expression, "CallExpression") || isNodeOfType(expression, "NewExpression")) return expression;
41022
- if (isNodeOfType(expression, "ChainExpression")) return findEagerInitializerCall(expression.expression, depth + 1);
41023
- if (isNodeOfType(expression, "LogicalExpression")) return findEagerInitializerCall(expression.left, depth + 1);
41024
- if (isNodeOfType(expression, "MemberExpression")) return findEagerInitializerCall(expression.object, depth + 1);
41025
- if (isNodeOfType(expression, "ArrayExpression")) for (const element of expression.elements ?? []) {
41613
+ const innerExpression = stripParenExpression(expression);
41614
+ if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
41615
+ if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
41616
+ if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
41617
+ if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
41026
41618
  if (!element || !isNodeOfType(element, "SpreadElement")) continue;
41027
41619
  const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
41028
41620
  if (spreadCall) return spreadCall;
@@ -41045,7 +41637,7 @@ const rerenderLazyStateInit = defineRule({
41045
41637
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
41046
41638
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
41047
41639
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
41048
- if (isConstructor && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
41640
+ if (isTrivialBuiltInConstruction(initializer)) return;
41049
41641
  if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
41050
41642
  if (isReactHookName(calleeName)) return;
41051
41643
  const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
@@ -41712,7 +42304,7 @@ const rnAnimationReactionAsDerived = defineRule({
41712
42304
  const body = reactionFn.body;
41713
42305
  let singleAssignment = null;
41714
42306
  if (isNodeOfType(body, "BlockStatement")) {
41715
- const statements = body.body ?? [];
42307
+ const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
41716
42308
  if (statements.length !== 1) return;
41717
42309
  const onlyStatement = statements[0];
41718
42310
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
@@ -41786,7 +42378,8 @@ const PROMISE_SETTLE_METHODS = new Set([
41786
42378
  "catch",
41787
42379
  "finally"
41788
42380
  ]);
41789
- const findChainRoot = (node) => {
42381
+ const findChainRoot = (wrappedNode) => {
42382
+ const node = stripParenExpression(wrappedNode);
41790
42383
  if (isNodeOfType(node, "CallExpression")) {
41791
42384
  if (isNodeOfType(node.callee, "Identifier")) return {
41792
42385
  calleeName: node.callee.name,
@@ -42374,20 +42967,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
42374
42967
  };
42375
42968
  const isReactNativeDimensionsCallee = (node, callee) => {
42376
42969
  if (!isNodeOfType(callee, "MemberExpression")) return false;
42377
- if (isNodeOfType(callee.object, "Identifier")) {
42378
- const localName = callee.object.name;
42970
+ const receiver = stripParenExpression(callee.object);
42971
+ if (isNodeOfType(receiver, "Identifier")) {
42972
+ const localName = receiver.name;
42379
42973
  const binding = findVariableInitializer(node, localName);
42380
42974
  if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
42381
42975
  return localName === "Dimensions";
42382
42976
  }
42383
- if (isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions") {
42384
- if (getInitializerModuleSource(node, callee.object.object) === REACT_NATIVE_MODULE) return true;
42385
- const rootName = getRootIdentifierName(callee.object.object);
42977
+ if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
42978
+ if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
42979
+ const rootName = getRootIdentifierName(receiver.object);
42386
42980
  if (rootName === null) return false;
42387
42981
  const importBinding = getImportBindingForName(node, rootName);
42388
42982
  return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
42389
42983
  }
42390
- if (getRequireCallSource(callee.object) === REACT_NATIVE_MODULE) return isNodeOfType(callee.object, "MemberExpression") && !callee.object.computed && isNodeOfType(callee.object.property, "Identifier") && callee.object.property.name === "Dimensions";
42984
+ if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
42391
42985
  return false;
42392
42986
  };
42393
42987
  const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
@@ -42831,7 +43425,8 @@ const rnNoLegacyShadowStyles = defineRule({
42831
43425
  },
42832
43426
  CallExpression(node) {
42833
43427
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
42834
- if (!isNodeOfType(node.callee.object, "Identifier") || node.callee.object.name !== "StyleSheet") return;
43428
+ const receiver = stripParenExpression(node.callee.object);
43429
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
42835
43430
  if (!isMemberProperty(node.callee, "create")) return;
42836
43431
  const stylesArgument = node.arguments?.[0];
42837
43432
  if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
@@ -43681,7 +44276,7 @@ const rnNoSetNativeProps = defineRule({
43681
44276
  const callee = node.callee;
43682
44277
  if (!isStaticMemberNamed(callee, "setNativeProps")) return;
43683
44278
  if (!isNodeOfType(callee, "MemberExpression")) return;
43684
- if (!isStaticMemberNamed(callee.object, "current")) return;
44279
+ if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
43685
44280
  context.report({
43686
44281
  node,
43687
44282
  message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
@@ -43909,14 +44504,15 @@ const analyzeGestureChain = (expression) => {
43909
44504
  if (!isNodeOfType(callee, "MemberExpression")) return null;
43910
44505
  if (!isNodeOfType(callee.property, "Identifier")) return null;
43911
44506
  const methodName = callee.property.name;
43912
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
44507
+ const receiver = stripParenExpression(callee.object);
44508
+ if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
43913
44509
  factoryName: methodName,
43914
44510
  chainMethodNames,
43915
44511
  numberOfTapsArgument
43916
44512
  };
43917
44513
  if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
43918
44514
  chainMethodNames.push(methodName);
43919
- cursor = callee.object;
44515
+ cursor = receiver;
43920
44516
  }
43921
44517
  return null;
43922
44518
  };
@@ -47582,6 +48178,8 @@ const roleSupportsAriaProps = defineRule({
47582
48178
  const propName = propRawName.toLowerCase();
47583
48179
  if (!propName.startsWith("aria-")) continue;
47584
48180
  if (!ARIA_PROPERTIES.has(propName)) continue;
48181
+ const attributeValue = attributeNode.value;
48182
+ if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
47585
48183
  (ariaAttributes ??= []).push({
47586
48184
  attribute,
47587
48185
  propName
@@ -47960,7 +48558,11 @@ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47960
48558
  const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47961
48559
  return Boolean(symbol && symbol.kind !== "import");
47962
48560
  };
47963
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
48561
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
48562
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
48563
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
48564
+ return false;
48565
+ };
47964
48566
  const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47965
48567
  const initializer = symbol.initializer;
47966
48568
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
@@ -48304,7 +48906,8 @@ const serverAfterNonblocking = defineRule({
48304
48906
  if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
48305
48907
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
48306
48908
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
48307
- const objectName = isNodeOfType(node.callee.object, "Identifier") ? node.callee.object.name : null;
48909
+ const receiver = stripParenExpression(node.callee.object);
48910
+ const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
48308
48911
  if (!objectName) return;
48309
48912
  const methodName = node.callee.property.name;
48310
48913
  if (!isDeferrableSideEffectCall(objectName, methodName)) return;
@@ -48980,7 +49583,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
48980
49583
  };
48981
49584
  const ascendMemberChain = (referenceIdentifier) => {
48982
49585
  let chainTip = referenceIdentifier;
48983
- while (chainTip.parent && isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) chainTip = chainTip.parent;
49586
+ while (chainTip.parent) {
49587
+ if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
49588
+ chainTip = chainTip.parent;
49589
+ continue;
49590
+ }
49591
+ if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
49592
+ chainTip = chainTip.parent;
49593
+ continue;
49594
+ }
49595
+ break;
49596
+ }
48984
49597
  return chainTip;
48985
49598
  };
48986
49599
  const isDirectContentsMutation = (referenceIdentifier) => {
@@ -49005,7 +49618,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
49005
49618
  const callee = callExpression.callee;
49006
49619
  if (isNodeOfType(callee, "MemberExpression")) {
49007
49620
  const methodName = getMemberPropertyName$1(callee);
49008
- return Boolean(isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49621
+ const calleeReceiver = stripParenExpression(callee.object);
49622
+ return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
49009
49623
  }
49010
49624
  if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
49011
49625
  const calleeSymbol = scopes.symbolFor(callee);
@@ -49735,7 +50349,7 @@ const walkServerFnChain = (outerNode) => {
49735
50349
  };
49736
50350
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
49737
50351
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
49738
- let currentNode = outerNode.callee.object;
50352
+ let currentNode = stripParenExpression(outerNode.callee.object);
49739
50353
  while (isNodeOfType(currentNode, "CallExpression")) {
49740
50354
  const calleeName = getCalleeName$2(currentNode);
49741
50355
  if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
@@ -49746,7 +50360,7 @@ const walkServerFnChain = (outerNode) => {
49746
50360
  }
49747
50361
  }
49748
50362
  if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
49749
- if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
50363
+ if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
49750
50364
  else break;
49751
50365
  }
49752
50366
  return result;
@@ -50399,7 +51013,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
50399
51013
  while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
50400
51014
  const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
50401
51015
  if (methodName) methodNames.unshift(methodName);
50402
- currentNode = currentNode.callee.object;
51016
+ currentNode = stripParenExpression(currentNode.callee.object);
50403
51017
  }
50404
51018
  if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
50405
51019
  if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
@@ -53481,6 +54095,18 @@ const reactDoctorRules = [
53481
54095
  category: "Bugs"
53482
54096
  }
53483
54097
  },
54098
+ {
54099
+ key: "react-doctor/no-locale-format-in-render",
54100
+ id: "no-locale-format-in-render",
54101
+ source: "react-doctor",
54102
+ originallyExternal: false,
54103
+ rule: {
54104
+ ...noLocaleFormatInRender,
54105
+ framework: "global",
54106
+ category: "Bugs",
54107
+ requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
54108
+ }
54109
+ },
53484
54110
  {
53485
54111
  key: "react-doctor/no-long-transition-duration",
53486
54112
  id: "no-long-transition-duration",