oxlint-plugin-react-doctor 0.7.2-dev.08b768b → 0.7.2-dev.0eb5293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +292 -602
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -532,12 +532,6 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
532
532
  ]);
533
533
  const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
534
534
  const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
535
- const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
536
- "WebSocket",
537
- "EventSource",
538
- "BroadcastChannel",
539
- "RTCPeerConnection"
540
- ]);
541
535
  const MUTABLE_GLOBAL_ROOTS = new Set([
542
536
  "location",
543
537
  "window",
@@ -601,19 +595,6 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
601
595
  "parseInt",
602
596
  "parseFloat"
603
597
  ]);
604
- const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
605
- "Date",
606
- "Map",
607
- "Set",
608
- "WeakMap",
609
- "WeakSet",
610
- "WeakRef",
611
- "RegExp",
612
- "Error",
613
- "URL",
614
- "URLSearchParams",
615
- "AbortController"
616
- ]);
617
598
  const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
618
599
  "Boolean",
619
600
  "String",
@@ -693,10 +674,7 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
693
674
  "unwatch",
694
675
  "unlisten",
695
676
  "unsub",
696
- "abort",
697
- "disconnect",
698
- "unobserve",
699
- "close"
677
+ "abort"
700
678
  ]);
701
679
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
702
680
  "remove",
@@ -2257,38 +2235,6 @@ const anchorHasContent = defineRule({
2257
2235
  }
2258
2236
  });
2259
2237
  //#endregion
2260
- //#region src/plugin/utils/get-jsx-prop-static-string-values.ts
2261
- const MAX_CONST_RESOLUTION_HOPS = 4;
2262
- const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2263
- const expression = stripParenExpression(rawExpression);
2264
- if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
2265
- if (isNodeOfType(expression, "TemplateLiteral")) {
2266
- const staticValue = getStaticTemplateLiteralValue(expression);
2267
- return staticValue === null ? null : [staticValue];
2268
- }
2269
- if (isNodeOfType(expression, "ConditionalExpression")) {
2270
- const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
2271
- if (consequentValues === null) return null;
2272
- const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
2273
- if (alternateValues === null) return null;
2274
- return [...consequentValues, ...alternateValues];
2275
- }
2276
- if (isNodeOfType(expression, "Identifier")) {
2277
- if (remainingHops === 0) return null;
2278
- const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2279
- if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
- return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2281
- }
2282
- return null;
2283
- };
2284
- const getJsxPropStaticStringValues = (attribute, scopes) => {
2285
- const value = attribute.value;
2286
- if (!value) return null;
2287
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
2288
- if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
2289
- return null;
2290
- };
2291
- //#endregion
2292
2238
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2293
2239
  const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
2294
2240
  const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
@@ -2318,11 +2264,28 @@ const isInvalidHref = (value, validHrefs) => {
2318
2264
  if (validHrefs.has(value)) return false;
2319
2265
  return value === "" || value === "#" || value === "javascript:void(0)";
2320
2266
  };
2321
- const isNullishOrFragmentHref = (value) => {
2267
+ const getStaticHrefValue = (value) => {
2268
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
2269
+ if (isNodeOfType(value, "JSXExpressionContainer")) {
2270
+ const expression = value.expression;
2271
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
2272
+ if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
2273
+ }
2274
+ return null;
2275
+ };
2276
+ const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
2277
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
2322
2278
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2323
2279
  const expression = value.expression;
2324
2280
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2325
- if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2281
+ if (isNodeOfType(expression, "Literal")) {
2282
+ if (expression.value === null) return true;
2283
+ if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2284
+ }
2285
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2286
+ const staticValue = getStaticTemplateLiteralValue(expression);
2287
+ return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2288
+ }
2326
2289
  }
2327
2290
  if (isNodeOfType(value, "JSXFragment")) return true;
2328
2291
  return false;
@@ -2355,10 +2318,9 @@ const anchorIsValid = defineRule({
2355
2318
  });
2356
2319
  return;
2357
2320
  }
2358
- const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2359
- if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2321
+ if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2360
2322
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2361
- if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2323
+ if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2362
2324
  context.report({
2363
2325
  node: node.name,
2364
2326
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3414,25 +3376,6 @@ const ariaRole = defineRule({
3414
3376
  if (!roleAttribute) return;
3415
3377
  const elementType = getElementType(node, context.settings);
3416
3378
  if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
3417
- const reportFirstInvalidCandidate = (candidates) => {
3418
- for (const candidate of candidates) {
3419
- if (candidate.trim().length === 0) {
3420
- context.report({
3421
- node: roleAttribute,
3422
- message: buildBaseMessage("")
3423
- });
3424
- return;
3425
- }
3426
- const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
3427
- for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3428
- context.report({
3429
- node: roleAttribute,
3430
- message: buildBaseMessage(` \`${token}\` is not one.`)
3431
- });
3432
- return;
3433
- }
3434
- }
3435
- };
3436
3379
  const value = roleAttribute.value;
3437
3380
  if (!value) {
3438
3381
  context.report({
@@ -3449,7 +3392,22 @@ const ariaRole = defineRule({
3449
3392
  });
3450
3393
  return;
3451
3394
  }
3452
- reportFirstInvalidCandidate([value.value]);
3395
+ const stringValue = value.value;
3396
+ if (stringValue.trim().length === 0) {
3397
+ context.report({
3398
+ node: roleAttribute,
3399
+ message: buildBaseMessage("")
3400
+ });
3401
+ return;
3402
+ }
3403
+ const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
3404
+ for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3405
+ context.report({
3406
+ node: roleAttribute,
3407
+ message: buildBaseMessage(` \`${token}\` is not one.`)
3408
+ });
3409
+ return;
3410
+ }
3453
3411
  return;
3454
3412
  }
3455
3413
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3470,11 +3428,6 @@ const ariaRole = defineRule({
3470
3428
  });
3471
3429
  return;
3472
3430
  }
3473
- const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3474
- if (resolvedCandidates !== null) {
3475
- reportFirstInvalidCandidate(resolvedCandidates);
3476
- return;
3477
- }
3478
3431
  return;
3479
3432
  }
3480
3433
  context.report({
@@ -7657,95 +7610,6 @@ const displayName = defineRule({
7657
7610
  }
7658
7611
  });
7659
7612
  //#endregion
7660
- //#region src/plugin/utils/is-react-hook-name.ts
7661
- const isReactHookName = (name) => {
7662
- if (!name.startsWith("use")) return false;
7663
- if (name.length === 3) return true;
7664
- const fourthCharacter = name.charCodeAt(3);
7665
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
7666
- };
7667
- //#endregion
7668
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
7669
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
7670
- //#endregion
7671
- //#region src/plugin/utils/component-or-hook-display-name.ts
7672
- const hocWrapperCalleeName = (callee) => {
7673
- if (isNodeOfType(callee, "Identifier")) return callee.name;
7674
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
7675
- return null;
7676
- };
7677
- const displayNameFromFunctionBinding = (functionNode) => {
7678
- let current = functionNode;
7679
- for (;;) {
7680
- const parent = current.parent;
7681
- if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
7682
- const calleeName = hocWrapperCalleeName(parent.callee);
7683
- if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
7684
- current = parent;
7685
- continue;
7686
- }
7687
- }
7688
- break;
7689
- }
7690
- const binding = current.parent;
7691
- if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
7692
- return null;
7693
- };
7694
- const componentOrHookDisplayNameForFunction = (functionNode) => {
7695
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
7696
- return displayNameFromFunctionBinding(functionNode);
7697
- };
7698
- //#endregion
7699
- //#region src/plugin/utils/find-enclosing-function.ts
7700
- const findEnclosingFunction = (node) => {
7701
- let cursor = node.parent;
7702
- while (cursor) {
7703
- if (isFunctionLike$1(cursor)) return cursor;
7704
- cursor = cursor.parent ?? null;
7705
- }
7706
- return null;
7707
- };
7708
- //#endregion
7709
- //#region src/plugin/utils/enclosing-component-or-hook-name.ts
7710
- const enclosingComponentOrHookName = (node) => {
7711
- const functionNode = findEnclosingFunction(node);
7712
- return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
7713
- };
7714
- //#endregion
7715
- //#region src/plugin/utils/is-result-discarded-call.ts
7716
- const isResultDiscardedCall = (callExpression) => {
7717
- let node = callExpression;
7718
- let parent = node.parent;
7719
- while (parent) {
7720
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
7721
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
7722
- if (isNodeOfType(parent, "ChainExpression")) {
7723
- node = parent;
7724
- parent = node.parent;
7725
- continue;
7726
- }
7727
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
7728
- node = parent;
7729
- parent = node.parent;
7730
- continue;
7731
- }
7732
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
7733
- node = parent;
7734
- parent = node.parent;
7735
- continue;
7736
- }
7737
- if (isNodeOfType(parent, "SequenceExpression")) {
7738
- const expressions = parent.expressions ?? [];
7739
- if (expressions[expressions.length - 1] !== node) return true;
7740
- node = parent;
7741
- parent = node.parent;
7742
- continue;
7743
- }
7744
- return false;
7745
- }
7746
- return false;
7747
- };
7748
- //#endregion
7749
7613
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7750
7614
  const walkInsideStatementBlocks = (node, visitor) => {
7751
7615
  if (!node || typeof node !== "object") return;
@@ -7897,17 +7761,6 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7897
7761
  };
7898
7762
  //#endregion
7899
7763
  //#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
7900
- const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
7901
- const RESOURCE_NOUN_BY_KIND = {
7902
- subscribe: "subscription",
7903
- timer: "timer",
7904
- socket: "connection"
7905
- };
7906
- const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
7907
- const isSubscribeOrObserveCall = (node) => {
7908
- if (isSubscribeLikeCallExpression(node)) return true;
7909
- return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
7910
- };
7911
7764
  const findSubscribeLikeUsages = (callback) => {
7912
7765
  const usages = [];
7913
7766
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7919,14 +7772,6 @@ const findSubscribeLikeUsages = (callback) => {
7919
7772
  }
7920
7773
  walkAst(callback, (child) => {
7921
7774
  if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
7922
- if (isSocketConstruction(child)) {
7923
- usages.push({
7924
- kind: "socket",
7925
- node: child,
7926
- resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
7927
- });
7928
- return;
7929
- }
7930
7775
  if (!isNodeOfType(child, "CallExpression")) return;
7931
7776
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7932
7777
  usages.push({
@@ -7936,7 +7781,7 @@ const findSubscribeLikeUsages = (callback) => {
7936
7781
  });
7937
7782
  return;
7938
7783
  }
7939
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
7784
+ if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
7940
7785
  kind: "subscribe",
7941
7786
  node: child,
7942
7787
  resourceName: child.callee.property.name
@@ -7959,12 +7804,7 @@ const collectCleanupBindings = (effectCallback) => {
7959
7804
  const bindingName = declarator.id.name;
7960
7805
  bindings.effectScopeVariableNames.add(bindingName);
7961
7806
  const init = declarator.init;
7962
- if (!init) continue;
7963
- if (isSocketConstruction(init)) {
7964
- bindings.subscriptionNames.add(bindingName);
7965
- continue;
7966
- }
7967
- if (!isNodeOfType(init, "CallExpression")) continue;
7807
+ if (!init || !isNodeOfType(init, "CallExpression")) continue;
7968
7808
  if (isSubscribeLikeCallExpression(init)) {
7969
7809
  bindings.subscriptionNames.add(bindingName);
7970
7810
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -8016,168 +7856,26 @@ const effectHasCleanupReturn = (callback, usages) => {
8016
7856
  });
8017
7857
  return didFindCleanupReturn;
8018
7858
  };
8019
- 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)));
8021
- const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
8022
- ["addEventListener", new Set(["removeEventListener", "abort"])],
8023
- ["addListener", new Set([
8024
- "removeListener",
8025
- "off",
8026
- "abort"
8027
- ])],
8028
- ["on", new Set([
8029
- "off",
8030
- "removeListener",
8031
- "on"
8032
- ])],
8033
- ["subscribe", new Set(["unsubscribe", "unsub"])],
8034
- ["sub", new Set(["unsub", "unsubscribe"])],
8035
- ["watch", new Set(["unwatch", "close"])],
8036
- ["listen", new Set(["unlisten", "close"])],
8037
- [OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
8038
- ]);
8039
- const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
8040
- "cleanup",
8041
- "dispose",
8042
- "destroy",
8043
- "teardown"
8044
- ]);
8045
- const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
8046
- const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
8047
- const getReleaseVerbName = (node) => {
8048
- if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
8049
- const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
8050
- if (!isNodeOfType(callNode, "CallExpression")) return null;
8051
- const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
8052
- if (isNodeOfType(callee, "Identifier")) return callee.name;
8053
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
8054
- return null;
8055
- };
8056
- const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
8057
- const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
8058
- let didFindPairedRelease = false;
8059
- walkAst(body, (child) => {
8060
- if (didFindPairedRelease) return false;
8061
- if (child !== body && isFunctionLike$1(child)) return false;
8062
- const releaseVerbName = getReleaseVerbName(child);
8063
- if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
8064
- didFindPairedRelease = true;
8065
- return false;
8066
- }
8067
- });
8068
- return didFindPairedRelease;
8069
- };
8070
- const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
8071
- const collectFileReleaseVerbNames = (anyNode) => {
8072
- let programNode = anyNode;
8073
- while (programNode.parent) programNode = programNode.parent;
8074
- const cached = fileReleaseVerbNamesCache.get(programNode);
8075
- if (cached) return cached;
8076
- const releaseVerbNames = /* @__PURE__ */ new Set();
8077
- walkAst(programNode, (child) => {
8078
- const releaseVerbName = getReleaseVerbName(child);
8079
- if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
8080
- });
8081
- fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
8082
- return releaseVerbNames;
8083
- };
8084
- const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
8085
- const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
8086
- const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
8087
- if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
8088
- for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
8089
- return false;
8090
- };
8091
- const findRetainedFunctionLeak = (retainedFunction) => {
8092
- if (!isFunctionLike$1(retainedFunction)) return null;
8093
- const body = retainedFunction.body;
8094
- if (!body) return null;
8095
- let leak = null;
8096
- walkAst(body, (child) => {
8097
- if (leak !== null) return false;
8098
- if (isFunctionLike$1(child)) return false;
8099
- if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
8100
- leak = {
8101
- kind: "socket",
8102
- node: child,
8103
- resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
8104
- };
8105
- return false;
8106
- }
8107
- if (!isNodeOfType(child, "CallExpression")) return;
8108
- if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
8109
- leak = {
8110
- kind: "timer",
8111
- node: child,
8112
- resourceName: "setInterval"
8113
- };
8114
- return false;
8115
- }
8116
- if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
8117
- const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
8118
- if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
8119
- kind: "subscribe",
8120
- node: child,
8121
- resourceName: registrationVerbName
8122
- };
8123
- return false;
8124
- }
8125
- });
8126
- return leak;
8127
- };
8128
- const isRetainedComponentScopeFunction = (functionNode) => {
8129
- if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
8130
- if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
8131
- if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
8132
- return enclosingComponentOrHookName(functionNode) !== null;
8133
- };
8134
7859
  const effectNeedsCleanup = defineRule({
8135
7860
  id: "effect-needs-cleanup",
8136
7861
  title: "Effect subscription or timer never cleaned up",
8137
7862
  severity: "error",
8138
7863
  tags: ["test-noise"],
8139
- recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
8140
- create: (context) => {
8141
- const reportRetainedLeak = (retainedFunction) => {
8142
- const leak = findRetainedFunctionLeak(retainedFunction);
8143
- if (!leak) return;
8144
- const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
8145
- context.report({
8146
- node: leak.node,
8147
- message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
8148
- });
8149
- };
8150
- return {
8151
- CallExpression(node) {
8152
- if (isHookCall$2(node, "useCallback")) {
8153
- const retainedCallback = getEffectCallback(node);
8154
- if (retainedCallback) reportRetainedLeak(retainedCallback);
8155
- return;
8156
- }
8157
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
8158
- const callback = getEffectCallback(node);
8159
- if (!callback) return;
8160
- const usages = findSubscribeLikeUsages(callback);
8161
- if (usages.length === 0) return;
8162
- if (effectHasCleanupReturn(callback, usages)) return;
8163
- const firstUsage = usages[0];
8164
- const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
8165
- context.report({
8166
- node,
8167
- message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
8168
- });
8169
- },
8170
- FunctionDeclaration(node) {
8171
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8172
- },
8173
- ArrowFunctionExpression(node) {
8174
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8175
- },
8176
- FunctionExpression(node) {
8177
- if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
8178
- }
8179
- };
8180
- }
7864
+ recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
7865
+ create: (context) => ({ CallExpression(node) {
7866
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7867
+ const callback = getEffectCallback(node);
7868
+ if (!callback) return;
7869
+ const usages = findSubscribeLikeUsages(callback);
7870
+ if (usages.length === 0) return;
7871
+ if (effectHasCleanupReturn(callback, usages)) return;
7872
+ const firstUsage = usages[0];
7873
+ const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7874
+ context.report({
7875
+ node,
7876
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7877
+ });
7878
+ } })
8181
7879
  });
8182
7880
  //#endregion
8183
7881
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8736,6 +8434,17 @@ const closureCaptures = (functionNode, scopes) => {
8736
8434
  return computedCaptures;
8737
8435
  };
8738
8436
  //#endregion
8437
+ //#region src/plugin/utils/is-react-hook-name.ts
8438
+ const isReactHookName = (name) => {
8439
+ if (!name.startsWith("use")) return false;
8440
+ if (name.length === 3) return true;
8441
+ const fourthCharacter = name.charCodeAt(3);
8442
+ return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8443
+ };
8444
+ //#endregion
8445
+ //#region src/plugin/utils/is-react-component-or-hook-name.ts
8446
+ const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8447
+ //#endregion
8739
8448
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8740
8449
  const reactHocCalleeName = (callee) => {
8741
8450
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -9907,7 +9616,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9907
9616
  });
9908
9617
  //#endregion
9909
9618
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9910
- const EMPTY_VISITORS$6 = {};
9619
+ const EMPTY_VISITORS$5 = {};
9911
9620
  const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
9912
9621
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9913
9622
  const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
@@ -9919,7 +9628,7 @@ const expoNoNonInlinedEnv = defineRule({
9919
9628
  recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
9920
9629
  create: (context) => {
9921
9630
  const filename = normalizeFilename(context.filename ?? "");
9922
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9631
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9923
9632
  return {
9924
9633
  MemberExpression(node) {
9925
9634
  if (!node.computed) return;
@@ -11337,8 +11046,8 @@ const interactiveSupportsFocus = defineRule({
11337
11046
  if (node.attributes.length === 0) return;
11338
11047
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11339
11048
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11340
- const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11341
- if (roleCandidates === null || roleCandidates.length === 0) return;
11049
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
+ if (!role) return;
11342
11051
  let hasInteractiveHandler = false;
11343
11052
  for (const attribute of node.attributes) {
11344
11053
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11352,16 +11061,11 @@ const interactiveSupportsFocus = defineRule({
11352
11061
  const elementType = getElementType(node, context.settings);
11353
11062
  if (!HTML_TAGS.has(elementType)) return;
11354
11063
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11064
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11065
+ if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11355
11066
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11356
- const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11357
- for (const role of roleCandidates) {
11358
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11359
- if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11360
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11361
- }
11362
- const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11363
- const roleDisplay = roleCandidates.join("' / '");
11364
- const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11067
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11068
+ const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
11365
11069
  context.report({
11366
11070
  node,
11367
11071
  message
@@ -11550,6 +11254,16 @@ const collectHandlerReferencedNames = (root) => {
11550
11254
  return names;
11551
11255
  };
11552
11256
  //#endregion
11257
+ //#region src/plugin/utils/find-enclosing-function.ts
11258
+ const findEnclosingFunction = (node) => {
11259
+ let cursor = node.parent;
11260
+ while (cursor) {
11261
+ if (isFunctionLike$1(cursor)) return cursor;
11262
+ cursor = cursor.parent ?? null;
11263
+ }
11264
+ return null;
11265
+ };
11266
+ //#endregion
11553
11267
  //#region src/plugin/utils/get-function-binding-name.ts
11554
11268
  const getFunctionBindingIdentifier = (functionNode) => {
11555
11269
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -21354,7 +21068,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21354
21068
  const noAdjustStateOnPropChange = defineRule({
21355
21069
  id: "no-adjust-state-on-prop-change",
21356
21070
  title: "State synced to a prop inside an effect",
21357
- severity: "warn",
21071
+ severity: "error",
21358
21072
  tags: ["test-noise"],
21359
21073
  recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
21360
21074
  create: (context) => ({ CallExpression(node) {
@@ -22763,10 +22477,6 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22763
22477
  const section = manifest[sectionName];
22764
22478
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22765
22479
  });
22766
- const declaresDependency = (manifest, dependencyName) => {
22767
- for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22768
- return false;
22769
- };
22770
22480
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22771
22481
  const classifyPackagePlatform = (filename) => {
22772
22482
  const manifest = readNearestPackageManifest(filename);
@@ -22783,7 +22493,9 @@ const classifyPackagePlatform = (filename) => {
22783
22493
  return result;
22784
22494
  };
22785
22495
  //#endregion
22786
- //#region src/plugin/utils/is-package-nested-below-project-root.ts
22496
+ //#region src/plugin/utils/is-react-native-file.ts
22497
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22498
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22787
22499
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22788
22500
  const resolveRealDirectory = (directory) => {
22789
22501
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22804,10 +22516,6 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22804
22516
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22805
22517
  return realPackageDirectory.startsWith(rootPrefix);
22806
22518
  };
22807
- //#endregion
22808
- //#region src/plugin/utils/is-react-native-file.ts
22809
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22810
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22811
22519
  const classifyReactNativeFileTarget = (context) => {
22812
22520
  const rawFilename = context.filename;
22813
22521
  if (!rawFilename) return "unknown";
@@ -23224,6 +22932,7 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
23224
22932
  let fallThroughCount = 0;
23225
22933
  let maxTerminatingPathCount = 0;
23226
22934
  for (const statement of statements) {
22935
+ if (isFunctionLike$1(statement)) continue;
23227
22936
  const guardBranch = isGuardWithTerminatingBranch(statement);
23228
22937
  if (guardBranch) {
23229
22938
  maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
@@ -23259,23 +22968,14 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
23259
22968
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
23260
22969
  return parent.parent === effectCallback.body;
23261
22970
  };
23262
- const countFunctionBodySetStateCalls = (functionNode, context) => {
23263
- if (isAsyncFunctionLike(functionNode)) return 0;
23264
- const body = functionNode.body;
23265
- if (!body) return 0;
23266
- return countMaxPathSetStateCalls(body, context);
23267
- };
23268
22971
  const countMaxPathSetStateCalls = (node, context) => {
23269
22972
  if (!node || typeof node !== "object") return 0;
23270
- if (isFunctionLike$1(node)) return 0;
22973
+ if (isAsyncFunctionLike(node)) return 0;
23271
22974
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
23272
22975
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
23273
22976
  const consequent = node.consequent;
23274
22977
  const alternate = node.alternate;
23275
- const testCount = countMaxPathSetStateCalls(node.test, context);
23276
- const thenCount = countMaxPathSetStateCalls(consequent, context);
23277
- const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
23278
- return testCount + Math.max(thenCount, elseCount);
22978
+ return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
23279
22979
  }
23280
22980
  if (isNodeOfType(node, "SwitchStatement")) {
23281
22981
  let maxRunSetters = 0;
@@ -23305,31 +23005,28 @@ const countMaxPathSetStateCalls = (node, context) => {
23305
23005
  }
23306
23006
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23307
23007
  let nestedSettersInArgs = 0;
23308
- for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23008
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23309
23009
  return 1 + nestedSettersInArgs;
23310
23010
  }
23311
23011
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23312
23012
  const helperFunction = context.helpersByName.get(node.callee.name);
23313
23013
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23314
23014
  context.activeHelpers.add(helperFunction);
23315
- let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23015
+ let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23316
23016
  context.activeHelpers.delete(helperFunction);
23317
23017
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23318
23018
  return helperCount;
23319
23019
  }
23320
23020
  }
23321
- const countChild = (child) => {
23322
- if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23323
- return countMaxPathSetStateCalls(child, context);
23324
- };
23021
+ const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23325
23022
  let total = 0;
23326
23023
  const nodeRecord = node;
23327
23024
  for (const key of Object.keys(nodeRecord)) {
23328
23025
  if (key === "parent") continue;
23329
23026
  const child = nodeRecord[key];
23330
23027
  if (Array.isArray(child)) {
23331
- for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
23332
- } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
23028
+ for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
23029
+ } else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
23333
23030
  }
23334
23031
  return total;
23335
23032
  };
@@ -23378,7 +23075,7 @@ const noCascadingSetState = defineRule({
23378
23075
  const callback = getEffectCallback(node);
23379
23076
  if (!callback) return;
23380
23077
  if (isDevOnlyGuardedEffect(callback)) return;
23381
- const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23078
+ const setStateCallCount = countMaxPathSetStateCalls(callback, {
23382
23079
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23383
23080
  activeHelpers: /* @__PURE__ */ new Set(),
23384
23081
  effectCallback: callback
@@ -23722,6 +23419,40 @@ const noCloneElement = defineRule({
23722
23419
  } })
23723
23420
  });
23724
23421
  //#endregion
23422
+ //#region src/plugin/utils/component-or-hook-display-name.ts
23423
+ const hocWrapperCalleeName = (callee) => {
23424
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
23425
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23426
+ return null;
23427
+ };
23428
+ const displayNameFromFunctionBinding = (functionNode) => {
23429
+ let current = functionNode;
23430
+ for (;;) {
23431
+ const parent = current.parent;
23432
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23433
+ const calleeName = hocWrapperCalleeName(parent.callee);
23434
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23435
+ current = parent;
23436
+ continue;
23437
+ }
23438
+ }
23439
+ break;
23440
+ }
23441
+ const binding = current.parent;
23442
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23443
+ return null;
23444
+ };
23445
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
23446
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23447
+ return displayNameFromFunctionBinding(functionNode);
23448
+ };
23449
+ //#endregion
23450
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23451
+ const enclosingComponentOrHookName = (node) => {
23452
+ const functionNode = findEnclosingFunction(node);
23453
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23454
+ };
23455
+ //#endregion
23725
23456
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23726
23457
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23727
23458
  const CONTEXT_MODULES = [
@@ -24735,26 +24466,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24735
24466
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24736
24467
  }
24737
24468
  };
24738
- const flattenGuardedStatements = (statements) => {
24739
- const flattened = [];
24740
- for (const statement of statements) {
24741
- if (isNodeOfType(statement, "ExpressionStatement")) {
24742
- flattened.push(statement);
24743
- continue;
24744
- }
24745
- if (isNodeOfType(statement, "IfStatement")) {
24746
- for (const branch of [statement.consequent, statement.alternate]) {
24747
- if (!branch) continue;
24748
- const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
24749
- if (flattenedBranch === null) return null;
24750
- flattened.push(...flattenedBranch);
24751
- }
24752
- continue;
24753
- }
24754
- return null;
24755
- }
24756
- return flattened;
24757
- };
24758
24469
  const noDerivedStateEffect = defineRule({
24759
24470
  id: "no-derived-state-effect",
24760
24471
  title: "Derived state stored in an effect",
@@ -24786,8 +24497,8 @@ const noDerivedStateEffect = defineRule({
24786
24497
  }
24787
24498
  }
24788
24499
  if (sawAnyDep && allDepsAreInitialOnly) return;
24789
- const statements = flattenGuardedStatements(getCallbackStatements(callback));
24790
- if (statements === null || statements.length === 0) return;
24500
+ const statements = getCallbackStatements(callback);
24501
+ if (statements.length === 0) return;
24791
24502
  if (!statements.every((statement) => {
24792
24503
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24793
24504
  const expression = statement.expression;
@@ -26582,11 +26293,7 @@ const noEffectEventInDeps = defineRule({
26582
26293
  const initializer = declaratorNode.init;
26583
26294
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26584
26295
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26585
- if (isNodeOfType(initializer.callee, "Identifier")) {
26586
- if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26587
- const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26588
- if (calleeSymbol && calleeSymbol.kind !== "import") return;
26589
- }
26296
+ if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26590
26297
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26591
26298
  } });
26592
26299
  return {
@@ -29266,7 +28973,7 @@ const extractReturnTypeAnnotation = (returnType) => {
29266
28973
  const noJsxElementType = defineRule({
29267
28974
  id: "no-jsx-element-type",
29268
28975
  title: "No JSX.Element",
29269
- severity: "warn",
28976
+ severity: "error",
29270
28977
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
29271
28978
  create: (context) => {
29272
28979
  let isJsxImported = false;
@@ -30405,6 +30112,7 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30405
30112
  "reverse",
30406
30113
  "sort"
30407
30114
  ]);
30115
+ const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30408
30116
  const OBJECT_MUTATION_METHODS = new Set([
30409
30117
  "assign",
30410
30118
  "defineProperties",
@@ -30478,6 +30186,7 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30478
30186
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30479
30187
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30480
30188
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30189
+ if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30481
30190
  }
30482
30191
  }
30483
30192
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30516,7 +30225,6 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30516
30225
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30517
30226
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30518
30227
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
30519
- if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30520
30228
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30521
30229
  });
30522
30230
  return mutations;
@@ -30749,7 +30457,7 @@ const noNestedComponentDefinition = defineRule({
30749
30457
  id: "no-nested-component-definition",
30750
30458
  title: "Component defined inside another component",
30751
30459
  tags: ["test-noise", "react-jsx-only"],
30752
- severity: "warn",
30460
+ severity: "error",
30753
30461
  category: "Correctness",
30754
30462
  recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
30755
30463
  create: (context) => {
@@ -32228,6 +31936,40 @@ const noPreventDefault = defineRule({
32228
31936
  }
32229
31937
  });
32230
31938
  //#endregion
31939
+ //#region src/plugin/utils/is-result-discarded-call.ts
31940
+ const isResultDiscardedCall = (callExpression) => {
31941
+ let node = callExpression;
31942
+ let parent = node.parent;
31943
+ while (parent) {
31944
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
31945
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31946
+ if (isNodeOfType(parent, "ChainExpression")) {
31947
+ node = parent;
31948
+ parent = node.parent;
31949
+ continue;
31950
+ }
31951
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31952
+ node = parent;
31953
+ parent = node.parent;
31954
+ continue;
31955
+ }
31956
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31957
+ node = parent;
31958
+ parent = node.parent;
31959
+ continue;
31960
+ }
31961
+ if (isNodeOfType(parent, "SequenceExpression")) {
31962
+ const expressions = parent.expressions ?? [];
31963
+ if (expressions[expressions.length - 1] !== node) return true;
31964
+ node = parent;
31965
+ parent = node.parent;
31966
+ continue;
31967
+ }
31968
+ return false;
31969
+ }
31970
+ return false;
31971
+ };
31972
+ //#endregion
32231
31973
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
32232
31974
  const isRefLatchGuardedEffect = (callbackBody) => {
32233
31975
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -32641,7 +32383,7 @@ const noReactDomDeprecatedApis = defineRule({
32641
32383
  });
32642
32384
  //#endregion
32643
32385
  //#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."]]);
32386
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
32645
32387
  const isVendoredShadcnUiFilename = (rawFilename) => {
32646
32388
  if (!rawFilename) return false;
32647
32389
  const filename = rawFilename.replaceAll("\\", "/");
@@ -32679,7 +32421,7 @@ const noReact19DeprecatedApis = defineRule({
32679
32421
  requires: ["react:19"],
32680
32422
  tags: ["test-noise", "migration-hint"],
32681
32423
  severity: "warn",
32682
- recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
32424
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
32683
32425
  create: (context) => {
32684
32426
  if (isVendoredShadcnUiFilename(context.filename)) return {};
32685
32427
  return deprecatedReactImportRule.create(buildOncePerApiContext(context));
@@ -33003,11 +32745,34 @@ const noRedundantShouldComponentUpdate = defineRule({
33003
32745
  });
33004
32746
  //#endregion
33005
32747
  //#region src/plugin/rules/architecture/no-render-in-render.ts
32748
+ const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32749
+ if (!symbol || visitedSymbols.has(symbol)) return false;
32750
+ visitedSymbols.add(symbol);
32751
+ if (isComponentParameterSymbol(symbol)) return true;
32752
+ if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
32753
+ const source = symbol.initializer;
32754
+ if (!source) return false;
32755
+ return initializerRootsInProps(source, scopes, visitedSymbols);
32756
+ };
32757
+ const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32758
+ if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
32759
+ if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
32760
+ return rootsInProps(node, scopes, visitedSymbols);
32761
+ };
32762
+ const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
32763
+ let current = node;
32764
+ while (isNodeOfType(current, "MemberExpression")) {
32765
+ if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
32766
+ current = current.object;
32767
+ }
32768
+ if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
32769
+ return false;
32770
+ };
33006
32771
  const isInsideComponentContext = (node) => {
33007
32772
  let cursor = node.parent;
33008
32773
  while (cursor) {
32774
+ if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
33009
32775
  if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
33010
- if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
33011
32776
  cursor = cursor.parent ?? null;
33012
32777
  }
33013
32778
  return false;
@@ -33020,21 +32785,21 @@ const functionBodyOf = (node) => {
33020
32785
  const containsHookCall = (body) => {
33021
32786
  let found = false;
33022
32787
  walkAst(body, (child) => {
33023
- if (found) return false;
33024
- if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
32788
+ if (found) return;
33025
32789
  if (!isNodeOfType(child, "CallExpression")) return;
33026
32790
  const name = getCalleeName$2(child);
33027
32791
  if (name && isReactHookName(name)) found = true;
33028
32792
  });
33029
32793
  return found;
33030
32794
  };
33031
- const isHookCallingRenderHelper = (symbol) => {
32795
+ const isModuleScopeHookFreeHelper = (symbol) => {
33032
32796
  if (!symbol) return false;
33033
32797
  const declaration = symbol.declarationNode;
33034
32798
  if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
33035
32799
  const body = functionBodyOf(declaration);
33036
32800
  if (!body) return false;
33037
- return containsHookCall(body);
32801
+ if (findEnclosingFunction(declaration) !== null) return false;
32802
+ return !containsHookCall(body);
33038
32803
  };
33039
32804
  const noRenderInRender = defineRule({
33040
32805
  id: "no-render-in-render",
@@ -33045,11 +32810,18 @@ const noRenderInRender = defineRule({
33045
32810
  create: (context) => ({ JSXExpressionContainer(node) {
33046
32811
  const expression = node.expression;
33047
32812
  if (!isNodeOfType(expression, "CallExpression")) return;
33048
- if (!isNodeOfType(expression.callee, "Identifier")) return;
33049
- const calleeName = expression.callee.name;
33050
- if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
32813
+ let calleeName = null;
32814
+ if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
32815
+ else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
32816
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
33051
32817
  if (!isInsideComponentContext(node)) return;
33052
- if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
32818
+ if (isNodeOfType(expression.callee, "Identifier")) {
32819
+ const calleeSymbol = context.scopes.symbolFor(expression.callee);
32820
+ if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
32821
+ if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
32822
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
32823
+ if (rootsInProps(expression.callee.object, context.scopes)) return;
32824
+ }
33053
32825
  context.report({
33054
32826
  node: expression,
33055
32827
  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.`
@@ -36398,28 +36170,6 @@ const isTriviallyCheapExpression = (node) => {
36398
36170
  if (isNodeOfType(innerExpression, "MemberExpression")) return false;
36399
36171
  return true;
36400
36172
  };
36401
- const isTrivialContainerLiteral = (node) => {
36402
- if (!node) return false;
36403
- const innerExpression = stripParenExpression(node);
36404
- if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
36405
- if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
36406
- return false;
36407
- };
36408
- const isNonEscapingRead = (identifier) => {
36409
- const parentNode = identifier.parent;
36410
- return isNodeOfType(parentNode, "MemberExpression") && parentNode.object === identifier;
36411
- };
36412
- const isMemoIdentityUnused = (memoCallNode, scopes) => {
36413
- const parentNode = memoCallNode.parent;
36414
- if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
36415
- if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoCallNode) return false;
36416
- const bindingTarget = parentNode.id;
36417
- if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
36418
- if (!isNodeOfType(bindingTarget, "Identifier")) return false;
36419
- const symbol = scopes.symbolFor(bindingTarget);
36420
- if (!symbol) return false;
36421
- return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
36422
- };
36423
36173
  const noUsememoSimpleExpression = defineRule({
36424
36174
  id: "no-usememo-simple-expression",
36425
36175
  title: "useMemo on a cheap value",
@@ -36441,17 +36191,9 @@ const noUsememoSimpleExpression = defineRule({
36441
36191
  let returnExpression = null;
36442
36192
  if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
36443
36193
  else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
36444
- if (!returnExpression) return;
36445
- if (isTriviallyCheapExpression(returnExpression)) {
36446
- context.report({
36447
- node,
36448
- message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36449
- });
36450
- return;
36451
- }
36452
- if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
36194
+ if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
36453
36195
  node,
36454
- message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
36196
+ message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
36455
36197
  });
36456
36198
  } })
36457
36199
  });
@@ -36833,7 +36575,8 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36833
36575
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36834
36576
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36835
36577
  const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
36836
- const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
36578
+ const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
36579
+ const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
36837
36580
  const DEFAULT_REACT_HOCS = [
36838
36581
  "memo",
36839
36582
  "forwardRef",
@@ -36904,20 +36647,6 @@ const isReactComponentInitializer = (expression, state) => {
36904
36647
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36905
36648
  return false;
36906
36649
  };
36907
- const objectExpressionBundlesComponents = (objectExpression, state) => {
36908
- for (const property of objectExpression.properties ?? []) {
36909
- if (!isNodeOfType(property, "Property")) continue;
36910
- const value = skipTsExpression(property.value);
36911
- if (isNodeOfType(value, "Identifier")) {
36912
- if (state.localComponentNames.has(value.name)) return true;
36913
- continue;
36914
- }
36915
- if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36916
- if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
36917
- if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36918
- }
36919
- return false;
36920
- };
36921
36650
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36922
36651
  if (initializer) {
36923
36652
  const expression = skipTsExpression(initializer);
@@ -36948,10 +36677,6 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36948
36677
  reportNode
36949
36678
  };
36950
36679
  }
36951
- if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
36952
- kind: "namespace-object",
36953
- reportNode
36954
- };
36955
36680
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36956
36681
  kind: "non-component",
36957
36682
  reportNode
@@ -36968,7 +36693,7 @@ const collectRelevantNodes = (programRoot) => {
36968
36693
  walkAst(programRoot, (child) => {
36969
36694
  const childType = child.type;
36970
36695
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36971
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36696
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
36972
36697
  });
36973
36698
  return {
36974
36699
  exportNodes,
@@ -37013,31 +36738,18 @@ const onlyExportComponents = defineRule({
37013
36738
  category: "Architecture",
37014
36739
  create: (context) => {
37015
36740
  const settings = resolveSettings$6(context.settings);
36741
+ const state = {
36742
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36743
+ allowExportNames: new Set(settings.allowExportNames),
36744
+ allowConstantExport: settings.allowConstantExport
36745
+ };
37016
36746
  return { Program(node) {
37017
36747
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
37018
36748
  const { exportNodes, componentCandidates } = collectRelevantNodes(node);
37019
- const localComponentNames = /* @__PURE__ */ new Set();
37020
- const state = {
37021
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
37022
- allowExportNames: new Set(settings.allowExportNames),
37023
- allowConstantExport: settings.allowConstantExport,
37024
- localComponentNames
37025
- };
37026
- for (const child of componentCandidates) {
37027
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
37028
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37029
- }
37030
- if (isNodeOfType(child, "ClassDeclaration") && child.id) {
37031
- if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
37032
- }
37033
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
37034
- 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);
37036
- }
37037
- }
37038
36749
  const exports = [];
37039
36750
  let hasReactExport = false;
37040
36751
  let hasAnyExports = false;
36752
+ const localComponents = [];
37041
36753
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
37042
36754
  for (const child of exportNodes) {
37043
36755
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -37108,14 +36820,7 @@ const onlyExportComponents = defineRule({
37108
36820
  });
37109
36821
  continue;
37110
36822
  }
37111
- if (isNodeOfType(stripped, "ObjectExpression")) {
37112
- context.report({
37113
- node: stripped,
37114
- message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
37115
- });
37116
- continue;
37117
- }
37118
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
36823
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
37119
36824
  context.report({
37120
36825
  node: stripped,
37121
36826
  message: ANONYMOUS_MESSAGE
@@ -37168,23 +36873,32 @@ const onlyExportComponents = defineRule({
37168
36873
  let entry;
37169
36874
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
37170
36875
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
37171
- else {
37172
- entry = {
37173
- kind: "non-component",
37174
- reportNode
37175
- };
37176
- if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
37177
- }
36876
+ else entry = {
36877
+ kind: "non-component",
36878
+ reportNode
36879
+ };
37178
36880
  if (isReExportFromSource && entry.kind !== "react-component") continue;
37179
36881
  exports.push(entry);
37180
36882
  }
37181
36883
  }
37182
36884
  }
37183
36885
  for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
37184
- for (const entry of exports) if (entry.kind === "namespace-object") context.report({
37185
- node: entry.reportNode,
37186
- message: NAMESPACE_OBJECT_MESSAGE
37187
- });
36886
+ const isInsideExport = (node) => {
36887
+ let walker = node.parent;
36888
+ while (walker) {
36889
+ if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
36890
+ walker = walker.parent ?? null;
36891
+ }
36892
+ return false;
36893
+ };
36894
+ for (const child of componentCandidates) {
36895
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36896
+ if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36897
+ }
36898
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36899
+ if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36900
+ }
36901
+ }
37188
36902
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
37189
36903
  if (entry.kind === "non-component") context.report({
37190
36904
  node: entry.reportNode,
@@ -37195,6 +36909,14 @@ const onlyExportComponents = defineRule({
37195
36909
  message: REACT_CONTEXT_MESSAGE
37196
36910
  });
37197
36911
  }
36912
+ else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
36913
+ node: local,
36914
+ message: LOCAL_COMPONENT_MESSAGE
36915
+ });
36916
+ else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
36917
+ node: local,
36918
+ message: NO_EXPORT_MESSAGE
36919
+ });
37198
36920
  } };
37199
36921
  }
37200
36922
  });
@@ -37992,8 +37714,8 @@ const preferHtmlDialog = defineRule({
37992
37714
  if (!HTML_TAGS.has(tagName)) return;
37993
37715
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37994
37716
  if (roleAttribute) {
37995
- const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
37996
- if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37717
+ const roleValue = getJsxPropStringValue(roleAttribute);
37718
+ if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
37997
37719
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37998
37720
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37999
37721
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -40982,7 +40704,6 @@ const rerenderLazyRefInit = defineRule({
40982
40704
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40983
40705
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40984
40706
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40985
- if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
40986
40707
  if (isPlainCall && isReactHookName(calleeName)) return;
40987
40708
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40988
40709
  context.report({
@@ -41015,6 +40736,18 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
41015
40736
  "getUTCMilliseconds",
41016
40737
  "valueOf"
41017
40738
  ]);
40739
+ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
40740
+ "Date",
40741
+ "Map",
40742
+ "Set",
40743
+ "WeakMap",
40744
+ "WeakSet",
40745
+ "RegExp",
40746
+ "Error",
40747
+ "URL",
40748
+ "URLSearchParams",
40749
+ "AbortController"
40750
+ ]);
41018
40751
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
41019
40752
  const findEagerInitializerCall = (expression, depth = 0) => {
41020
40753
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
@@ -41758,7 +41491,7 @@ const rnBottomSheetPreferNative = defineRule({
41758
41491
  });
41759
41492
  //#endregion
41760
41493
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41761
- const EMPTY_VISITORS$5 = {};
41494
+ const EMPTY_VISITORS$4 = {};
41762
41495
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41763
41496
  const DETOX_ELEMENT_ACTIONS = new Set([
41764
41497
  "tap",
@@ -41818,7 +41551,7 @@ const rnDetoxMissingAwait = defineRule({
41818
41551
  recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
41819
41552
  create: (context) => {
41820
41553
  const filename = normalizeFilename(context.filename ?? "");
41821
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41554
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
41822
41555
  return { ExpressionStatement(node) {
41823
41556
  const expression = node.expression;
41824
41557
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -42793,7 +42526,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42793
42526
  };
42794
42527
  //#endregion
42795
42528
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42796
- const EMPTY_VISITORS$4 = {};
42529
+ const EMPTY_VISITORS$3 = {};
42797
42530
  const reportLegacyShadowProperties = (objectExpression, context) => {
42798
42531
  const legacyShadowPropertyNames = [];
42799
42532
  for (const property of objectExpression.properties ?? []) {
@@ -42816,7 +42549,7 @@ const rnNoLegacyShadowStyles = defineRule({
42816
42549
  severity: "warn",
42817
42550
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42818
42551
  create: (context) => {
42819
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42552
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
42820
42553
  return {
42821
42554
  JSXAttribute(node) {
42822
42555
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -43737,7 +43470,7 @@ const isExpoManagedFileActive = (context) => {
43737
43470
  };
43738
43471
  //#endregion
43739
43472
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43740
- const EMPTY_VISITORS$3 = {};
43473
+ const EMPTY_VISITORS$2 = {};
43741
43474
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43742
43475
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43743
43476
  const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
@@ -43771,7 +43504,7 @@ const rnPreferExpoImage = defineRule({
43771
43504
  severity: "warn",
43772
43505
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43773
43506
  create: (context) => {
43774
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43507
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
43775
43508
  const flaggedImports = [];
43776
43509
  const assetBindingNames = /* @__PURE__ */ new Set();
43777
43510
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -44258,7 +43991,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
44258
43991
  });
44259
43992
  //#endregion
44260
43993
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
44261
- const EMPTY_VISITORS$2 = {};
43994
+ const EMPTY_VISITORS$1 = {};
44262
43995
  const IOS_SHADOW_KEYS = new Set([
44263
43996
  "shadowColor",
44264
43997
  "shadowOffset",
@@ -44344,7 +44077,7 @@ const rnStylePreferBoxShadow = defineRule({
44344
44077
  severity: "warn",
44345
44078
  recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
44346
44079
  create: (context) => {
44347
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44080
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
44348
44081
  return {
44349
44082
  JSXAttribute(node) {
44350
44083
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44441,9 +44174,9 @@ const roleHasRequiredAriaProps = defineRule({
44441
44174
  if (!HTML_TAGS.has(elementType)) return;
44442
44175
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44443
44176
  if (!roleAttribute) return;
44444
- const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
44445
- if (roleCandidates === null) return;
44446
- const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44177
+ const roleValue = getJsxPropStringValue(roleAttribute);
44178
+ if (roleValue === null) return;
44179
+ const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
44447
44180
  for (const role of roles) {
44448
44181
  const required = ROLE_REQUIRED_PROPS.get(role);
44449
44182
  if (!required) continue;
@@ -47560,9 +47293,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47560
47293
  };
47561
47294
  //#endregion
47562
47295
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47563
- const buildMessageDefault = (roles, propName) => {
47564
- return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
47565
- };
47296
+ const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
47566
47297
  const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
47567
47298
  const roleSupportsAriaProps = defineRule({
47568
47299
  id: "role-supports-aria-props",
@@ -47590,21 +47321,17 @@ const roleSupportsAriaProps = defineRule({
47590
47321
  if (!ariaAttributes) return;
47591
47322
  const elementType = getElementType(node, context.settings);
47592
47323
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47593
- const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
47594
- if (roleCandidates === null || roleCandidates.length === 0) return;
47595
- const supportedSets = [];
47596
- for (const role of roleCandidates) {
47597
- if (!VALID_ARIA_ROLES.has(role)) return;
47598
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47599
- if (!supported) return;
47600
- supportedSets.push(supported);
47601
- }
47324
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47325
+ if (!role) return;
47326
+ if (!VALID_ARIA_ROLES.has(role)) return;
47602
47327
  const isImplicit = !roleAttribute;
47328
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47329
+ if (!supported) return;
47603
47330
  for (const { attribute, propName } of ariaAttributes) {
47604
- if (supportedSets.some((supported) => supported.has(propName))) continue;
47331
+ if (supported.has(propName)) continue;
47605
47332
  context.report({
47606
47333
  node: attribute,
47607
- message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47334
+ message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
47608
47335
  });
47609
47336
  }
47610
47337
  } })
@@ -47956,15 +47683,11 @@ const isUseEffectEventSymbol = (symbol) => {
47956
47683
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47957
47684
  return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47958
47685
  };
47959
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47960
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47961
- return Boolean(symbol && symbol.kind !== "import");
47962
- };
47963
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47964
- const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47686
+ const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47687
+ const isNonReactEffectEventSymbol = (symbol, contextNode) => {
47965
47688
  const initializer = symbol.initializer;
47966
47689
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47967
- return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47690
+ return isNonReactEffectEventCallee(initializer.callee, contextNode);
47968
47691
  };
47969
47692
  const findEnclosingComponentOrHookFunction = (node) => {
47970
47693
  let current = node.parent;
@@ -48043,7 +47766,7 @@ const rulesOfHooks = defineRule({
48043
47766
  const hookContext = isHookCall$1(node, context.scopes, settings);
48044
47767
  if (!hookContext) return;
48045
47768
  const { hookName } = hookContext;
48046
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47769
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
48047
47770
  context.report({
48048
47771
  node: node.callee,
48049
47772
  message: buildEffectEventPassedDownMessage()
@@ -48156,7 +47879,7 @@ const rulesOfHooks = defineRule({
48156
47879
  Identifier(node) {
48157
47880
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
48158
47881
  if (!symbol || !isUseEffectEventSymbol(symbol)) return;
48159
- if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
47882
+ if (isNonReactEffectEventSymbol(symbol, node)) return;
48160
47883
  if (!isSameComponentOrHookScope(symbol, node)) return;
48161
47884
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
48162
47885
  context.report({
@@ -55728,34 +55451,6 @@ const reactDoctorRules = [
55728
55451
  ];
55729
55452
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55730
55453
  //#endregion
55731
- //#region src/plugin/utils/is-next-file.ts
55732
- const isNextFileActive = (context) => {
55733
- const rawFilename = context.filename;
55734
- if (!rawFilename) return true;
55735
- const filename = normalizeFilename(rawFilename);
55736
- const manifest = readNearestPackageManifest(filename);
55737
- if (!manifest) return true;
55738
- if (declaresDependency(manifest, "next")) return true;
55739
- if (!declaresAnyDependency(manifest)) return true;
55740
- const packageDirectory = findNearestPackageDirectory(filename);
55741
- const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
55742
- if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
55743
- return true;
55744
- };
55745
- //#endregion
55746
- //#region src/plugin/utils/wrap-nextjs-rule.ts
55747
- const EMPTY_VISITORS$1 = {};
55748
- const wrapNextjsRule = (rule) => {
55749
- const innerCreate = rule.create.bind(rule);
55750
- return {
55751
- ...rule,
55752
- create: (context) => {
55753
- if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
55754
- return innerCreate(context);
55755
- }
55756
- };
55757
- };
55758
- //#endregion
55759
55454
  //#region src/plugin/utils/wrap-react-native-rule.ts
55760
55455
  const EMPTY_VISITORS = {};
55761
55456
  const wrapReactNativeRule = (rule) => {
@@ -56225,14 +55920,9 @@ const wrapWithSemanticContext = (rule) => ({
56225
55920
  });
56226
55921
  //#endregion
56227
55922
  //#region src/plugin/react-doctor-plugin.ts
56228
- const applyFrameworkGate = (rule) => {
56229
- if (rule.framework === "react-native") return wrapReactNativeRule(rule);
56230
- if (rule.framework === "nextjs") return wrapNextjsRule(rule);
56231
- return rule;
56232
- };
56233
55923
  const applyFrameworkRuleWrappers = (registry) => {
56234
55924
  const wrapped = {};
56235
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55925
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
56236
55926
  return wrapped;
56237
55927
  };
56238
55928
  const plugin = {