oxlint-plugin-react-doctor 0.7.2-dev.43d766f → 0.7.2-dev.82e0475

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 +559 -249
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -532,6 +532,12 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
532
532
  ]);
533
533
  const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
534
534
  const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
535
+ const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
536
+ "WebSocket",
537
+ "EventSource",
538
+ "BroadcastChannel",
539
+ "RTCPeerConnection"
540
+ ]);
535
541
  const MUTABLE_GLOBAL_ROOTS = new Set([
536
542
  "location",
537
543
  "window",
@@ -595,6 +601,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
595
601
  "parseInt",
596
602
  "parseFloat"
597
603
  ]);
604
+ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
605
+ "Date",
606
+ "Map",
607
+ "Set",
608
+ "WeakMap",
609
+ "WeakSet",
610
+ "WeakRef",
611
+ "RegExp",
612
+ "Error",
613
+ "URL",
614
+ "URLSearchParams",
615
+ "AbortController"
616
+ ]);
598
617
  const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
599
618
  "Boolean",
600
619
  "String",
@@ -674,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
674
693
  "unwatch",
675
694
  "unlisten",
676
695
  "unsub",
677
- "abort"
696
+ "abort",
697
+ "disconnect",
698
+ "unobserve",
699
+ "close"
678
700
  ]);
679
701
  const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
680
702
  "remove",
@@ -2235,6 +2257,38 @@ const anchorHasContent = defineRule({
2235
2257
  }
2236
2258
  });
2237
2259
  //#endregion
2260
+ //#region src/plugin/utils/get-jsx-prop-static-string-values.ts
2261
+ const MAX_CONST_RESOLUTION_HOPS = 4;
2262
+ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2263
+ const expression = stripParenExpression(rawExpression);
2264
+ if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
2265
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2266
+ const staticValue = getStaticTemplateLiteralValue(expression);
2267
+ return staticValue === null ? null : [staticValue];
2268
+ }
2269
+ if (isNodeOfType(expression, "ConditionalExpression")) {
2270
+ const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
2271
+ if (consequentValues === null) return null;
2272
+ const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
2273
+ if (alternateValues === null) return null;
2274
+ return [...consequentValues, ...alternateValues];
2275
+ }
2276
+ if (isNodeOfType(expression, "Identifier")) {
2277
+ if (remainingHops === 0) return null;
2278
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2279
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2280
+ 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
2238
2292
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2239
2293
  const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
2240
2294
  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.";
@@ -2264,28 +2318,11 @@ const isInvalidHref = (value, validHrefs) => {
2264
2318
  if (validHrefs.has(value)) return false;
2265
2319
  return value === "" || value === "#" || value === "javascript:void(0)";
2266
2320
  };
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;
2321
+ const isNullishOrFragmentHref = (value) => {
2278
2322
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2279
2323
  const expression = value.expression;
2280
2324
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2281
- if (isNodeOfType(expression, "Literal")) {
2282
- if (expression.value === null) return true;
2283
- if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2284
- }
2285
- if (isNodeOfType(expression, "TemplateLiteral")) {
2286
- const staticValue = getStaticTemplateLiteralValue(expression);
2287
- return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2288
- }
2325
+ if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2289
2326
  }
2290
2327
  if (isNodeOfType(value, "JSXFragment")) return true;
2291
2328
  return false;
@@ -2318,9 +2355,10 @@ const anchorIsValid = defineRule({
2318
2355
  });
2319
2356
  return;
2320
2357
  }
2321
- if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2358
+ const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2359
+ if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2322
2360
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2323
- if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2361
+ if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2324
2362
  context.report({
2325
2363
  node: node.name,
2326
2364
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3376,6 +3414,25 @@ const ariaRole = defineRule({
3376
3414
  if (!roleAttribute) return;
3377
3415
  const elementType = getElementType(node, context.settings);
3378
3416
  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
+ };
3379
3436
  const value = roleAttribute.value;
3380
3437
  if (!value) {
3381
3438
  context.report({
@@ -3392,22 +3449,7 @@ const ariaRole = defineRule({
3392
3449
  });
3393
3450
  return;
3394
3451
  }
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
- }
3452
+ reportFirstInvalidCandidate([value.value]);
3411
3453
  return;
3412
3454
  }
3413
3455
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3428,6 +3470,11 @@ const ariaRole = defineRule({
3428
3470
  });
3429
3471
  return;
3430
3472
  }
3473
+ const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3474
+ if (resolvedCandidates !== null) {
3475
+ reportFirstInvalidCandidate(resolvedCandidates);
3476
+ return;
3477
+ }
3431
3478
  return;
3432
3479
  }
3433
3480
  context.report({
@@ -7610,6 +7657,95 @@ const displayName = defineRule({
7610
7657
  }
7611
7658
  });
7612
7659
  //#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
7613
7749
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
7614
7750
  const walkInsideStatementBlocks = (node, visitor) => {
7615
7751
  if (!node || typeof node !== "object") return;
@@ -7761,6 +7897,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
7761
7897
  };
7762
7898
  //#endregion
7763
7899
  //#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
+ };
7764
7911
  const findSubscribeLikeUsages = (callback) => {
7765
7912
  const usages = [];
7766
7913
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
@@ -7772,6 +7919,14 @@ const findSubscribeLikeUsages = (callback) => {
7772
7919
  }
7773
7920
  walkAst(callback, (child) => {
7774
7921
  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
+ }
7775
7930
  if (!isNodeOfType(child, "CallExpression")) return;
7776
7931
  if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
7777
7932
  usages.push({
@@ -7781,7 +7936,7 @@ const findSubscribeLikeUsages = (callback) => {
7781
7936
  });
7782
7937
  return;
7783
7938
  }
7784
- if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
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({
7785
7940
  kind: "subscribe",
7786
7941
  node: child,
7787
7942
  resourceName: child.callee.property.name
@@ -7804,7 +7959,12 @@ const collectCleanupBindings = (effectCallback) => {
7804
7959
  const bindingName = declarator.id.name;
7805
7960
  bindings.effectScopeVariableNames.add(bindingName);
7806
7961
  const init = declarator.init;
7807
- if (!init || !isNodeOfType(init, "CallExpression")) continue;
7962
+ if (!init) continue;
7963
+ if (isSocketConstruction(init)) {
7964
+ bindings.subscriptionNames.add(bindingName);
7965
+ continue;
7966
+ }
7967
+ if (!isNodeOfType(init, "CallExpression")) continue;
7808
7968
  if (isSubscribeLikeCallExpression(init)) {
7809
7969
  bindings.subscriptionNames.add(bindingName);
7810
7970
  if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
@@ -7856,26 +8016,168 @@ const effectHasCleanupReturn = (callback, usages) => {
7856
8016
  });
7857
8017
  return didFindCleanupReturn;
7858
8018
  };
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
+ };
7859
8134
  const effectNeedsCleanup = defineRule({
7860
8135
  id: "effect-needs-cleanup",
7861
8136
  title: "Effect subscription or timer never cleaned up",
7862
8137
  severity: "error",
7863
8138
  tags: ["test-noise"],
7864
- recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
7865
- create: (context) => ({ CallExpression(node) {
7866
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
7867
- const callback = getEffectCallback(node);
7868
- if (!callback) return;
7869
- const usages = findSubscribeLikeUsages(callback);
7870
- if (usages.length === 0) return;
7871
- if (effectHasCleanupReturn(callback, usages)) return;
7872
- const firstUsage = usages[0];
7873
- const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
7874
- context.report({
7875
- node,
7876
- message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
7877
- });
7878
- } })
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
+ }
7879
8181
  });
7880
8182
  //#endregion
7881
8183
  //#region src/plugin/semantic/scope-analysis.ts
@@ -8434,17 +8736,6 @@ const closureCaptures = (functionNode, scopes) => {
8434
8736
  return computedCaptures;
8435
8737
  };
8436
8738
  //#endregion
8437
- //#region src/plugin/utils/is-react-hook-name.ts
8438
- const isReactHookName = (name) => {
8439
- if (!name.startsWith("use")) return false;
8440
- if (name.length === 3) return true;
8441
- const fourthCharacter = name.charCodeAt(3);
8442
- return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
8443
- };
8444
- //#endregion
8445
- //#region src/plugin/utils/is-react-component-or-hook-name.ts
8446
- const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
8447
- //#endregion
8448
8739
  //#region src/plugin/utils/is-react-hoc-callback-argument.ts
8449
8740
  const reactHocCalleeName = (callee) => {
8450
8741
  if (isNodeOfType(callee, "Identifier")) return callee.name;
@@ -9616,7 +9907,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9616
9907
  });
9617
9908
  //#endregion
9618
9909
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9619
- const EMPTY_VISITORS$5 = {};
9910
+ const EMPTY_VISITORS$6 = {};
9620
9911
  const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
9621
9912
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9622
9913
  const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
@@ -9628,7 +9919,7 @@ const expoNoNonInlinedEnv = defineRule({
9628
9919
  recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
9629
9920
  create: (context) => {
9630
9921
  const filename = normalizeFilename(context.filename ?? "");
9631
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9922
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9632
9923
  return {
9633
9924
  MemberExpression(node) {
9634
9925
  if (!node.computed) return;
@@ -11046,8 +11337,8 @@ const interactiveSupportsFocus = defineRule({
11046
11337
  if (node.attributes.length === 0) return;
11047
11338
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11048
11339
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11049
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
- if (!role) return;
11340
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11341
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11051
11342
  let hasInteractiveHandler = false;
11052
11343
  for (const attribute of node.attributes) {
11053
11344
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11061,11 +11352,16 @@ const interactiveSupportsFocus = defineRule({
11061
11352
  const elementType = getElementType(node, context.settings);
11062
11353
  if (!HTML_TAGS.has(elementType)) return;
11063
11354
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11064
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11065
- if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11066
11355
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11067
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11068
- const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
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);
11069
11365
  context.report({
11070
11366
  node,
11071
11367
  message
@@ -11254,16 +11550,6 @@ const collectHandlerReferencedNames = (root) => {
11254
11550
  return names;
11255
11551
  };
11256
11552
  //#endregion
11257
- //#region src/plugin/utils/find-enclosing-function.ts
11258
- const findEnclosingFunction = (node) => {
11259
- let cursor = node.parent;
11260
- while (cursor) {
11261
- if (isFunctionLike$1(cursor)) return cursor;
11262
- cursor = cursor.parent ?? null;
11263
- }
11264
- return null;
11265
- };
11266
- //#endregion
11267
11553
  //#region src/plugin/utils/get-function-binding-name.ts
11268
11554
  const getFunctionBindingIdentifier = (functionNode) => {
11269
11555
  if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
@@ -21068,7 +21354,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
21068
21354
  const noAdjustStateOnPropChange = defineRule({
21069
21355
  id: "no-adjust-state-on-prop-change",
21070
21356
  title: "State synced to a prop inside an effect",
21071
- severity: "error",
21357
+ severity: "warn",
21072
21358
  tags: ["test-noise"],
21073
21359
  recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
21074
21360
  create: (context) => ({ CallExpression(node) {
@@ -22477,6 +22763,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22477
22763
  const section = manifest[sectionName];
22478
22764
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22479
22765
  });
22766
+ const declaresDependency = (manifest, dependencyName) => {
22767
+ for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22768
+ return false;
22769
+ };
22480
22770
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22481
22771
  const classifyPackagePlatform = (filename) => {
22482
22772
  const manifest = readNearestPackageManifest(filename);
@@ -22493,9 +22783,7 @@ const classifyPackagePlatform = (filename) => {
22493
22783
  return result;
22494
22784
  };
22495
22785
  //#endregion
22496
- //#region src/plugin/utils/is-react-native-file.ts
22497
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22498
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22786
+ //#region src/plugin/utils/is-package-nested-below-project-root.ts
22499
22787
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22500
22788
  const resolveRealDirectory = (directory) => {
22501
22789
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22516,6 +22804,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22516
22804
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22517
22805
  return realPackageDirectory.startsWith(rootPrefix);
22518
22806
  };
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?$/;
22519
22811
  const classifyReactNativeFileTarget = (context) => {
22520
22812
  const rawFilename = context.filename;
22521
22813
  if (!rawFilename) return "unknown";
@@ -22932,7 +23224,6 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
22932
23224
  let fallThroughCount = 0;
22933
23225
  let maxTerminatingPathCount = 0;
22934
23226
  for (const statement of statements) {
22935
- if (isFunctionLike$1(statement)) continue;
22936
23227
  const guardBranch = isGuardWithTerminatingBranch(statement);
22937
23228
  if (guardBranch) {
22938
23229
  maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
@@ -22968,14 +23259,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
22968
23259
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
22969
23260
  return parent.parent === effectCallback.body;
22970
23261
  };
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
+ };
22971
23268
  const countMaxPathSetStateCalls = (node, context) => {
22972
23269
  if (!node || typeof node !== "object") return 0;
22973
- if (isAsyncFunctionLike(node)) return 0;
23270
+ if (isFunctionLike$1(node)) return 0;
22974
23271
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
22975
23272
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
22976
23273
  const consequent = node.consequent;
22977
23274
  const alternate = node.alternate;
22978
- return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
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);
22979
23279
  }
22980
23280
  if (isNodeOfType(node, "SwitchStatement")) {
22981
23281
  let maxRunSetters = 0;
@@ -23005,28 +23305,31 @@ const countMaxPathSetStateCalls = (node, context) => {
23005
23305
  }
23006
23306
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23007
23307
  let nestedSettersInArgs = 0;
23008
- for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23308
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23009
23309
  return 1 + nestedSettersInArgs;
23010
23310
  }
23011
23311
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23012
23312
  const helperFunction = context.helpersByName.get(node.callee.name);
23013
23313
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23014
23314
  context.activeHelpers.add(helperFunction);
23015
- let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23315
+ let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23016
23316
  context.activeHelpers.delete(helperFunction);
23017
23317
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23018
23318
  return helperCount;
23019
23319
  }
23020
23320
  }
23021
- const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23321
+ const countChild = (child) => {
23322
+ if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23323
+ return countMaxPathSetStateCalls(child, context);
23324
+ };
23022
23325
  let total = 0;
23023
23326
  const nodeRecord = node;
23024
23327
  for (const key of Object.keys(nodeRecord)) {
23025
23328
  if (key === "parent") continue;
23026
23329
  const child = nodeRecord[key];
23027
23330
  if (Array.isArray(child)) {
23028
- for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
23029
- } else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
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);
23030
23333
  }
23031
23334
  return total;
23032
23335
  };
@@ -23075,7 +23378,7 @@ const noCascadingSetState = defineRule({
23075
23378
  const callback = getEffectCallback(node);
23076
23379
  if (!callback) return;
23077
23380
  if (isDevOnlyGuardedEffect(callback)) return;
23078
- const setStateCallCount = countMaxPathSetStateCalls(callback, {
23381
+ const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23079
23382
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23080
23383
  activeHelpers: /* @__PURE__ */ new Set(),
23081
23384
  effectCallback: callback
@@ -23419,40 +23722,6 @@ const noCloneElement = defineRule({
23419
23722
  } })
23420
23723
  });
23421
23724
  //#endregion
23422
- //#region src/plugin/utils/component-or-hook-display-name.ts
23423
- const hocWrapperCalleeName = (callee) => {
23424
- if (isNodeOfType(callee, "Identifier")) return callee.name;
23425
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
23426
- return null;
23427
- };
23428
- const displayNameFromFunctionBinding = (functionNode) => {
23429
- let current = functionNode;
23430
- for (;;) {
23431
- const parent = current.parent;
23432
- if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
23433
- const calleeName = hocWrapperCalleeName(parent.callee);
23434
- if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
23435
- current = parent;
23436
- continue;
23437
- }
23438
- }
23439
- break;
23440
- }
23441
- const binding = current.parent;
23442
- if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
23443
- return null;
23444
- };
23445
- const componentOrHookDisplayNameForFunction = (functionNode) => {
23446
- if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
23447
- return displayNameFromFunctionBinding(functionNode);
23448
- };
23449
- //#endregion
23450
- //#region src/plugin/utils/enclosing-component-or-hook-name.ts
23451
- const enclosingComponentOrHookName = (node) => {
23452
- const functionNode = findEnclosingFunction(node);
23453
- return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
23454
- };
23455
- //#endregion
23456
23725
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
23457
23726
  const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
23458
23727
  const CONTEXT_MODULES = [
@@ -24466,6 +24735,26 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
24466
24735
  } else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
24467
24736
  }
24468
24737
  };
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
+ };
24469
24758
  const noDerivedStateEffect = defineRule({
24470
24759
  id: "no-derived-state-effect",
24471
24760
  title: "Derived state stored in an effect",
@@ -24497,8 +24786,8 @@ const noDerivedStateEffect = defineRule({
24497
24786
  }
24498
24787
  }
24499
24788
  if (sawAnyDep && allDepsAreInitialOnly) return;
24500
- const statements = getCallbackStatements(callback);
24501
- if (statements.length === 0) return;
24789
+ const statements = flattenGuardedStatements(getCallbackStatements(callback));
24790
+ if (statements === null || statements.length === 0) return;
24502
24791
  if (!statements.every((statement) => {
24503
24792
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
24504
24793
  const expression = statement.expression;
@@ -26293,7 +26582,11 @@ const noEffectEventInDeps = defineRule({
26293
26582
  const initializer = declaratorNode.init;
26294
26583
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26295
26584
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26296
- if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) 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
+ }
26297
26590
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26298
26591
  } });
26299
26592
  return {
@@ -28973,7 +29266,7 @@ const extractReturnTypeAnnotation = (returnType) => {
28973
29266
  const noJsxElementType = defineRule({
28974
29267
  id: "no-jsx-element-type",
28975
29268
  title: "No JSX.Element",
28976
- severity: "error",
29269
+ severity: "warn",
28977
29270
  recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
28978
29271
  create: (context) => {
28979
29272
  let isJsxImported = false;
@@ -30112,7 +30405,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30112
30405
  "reverse",
30113
30406
  "sort"
30114
30407
  ]);
30115
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30116
30408
  const OBJECT_MUTATION_METHODS = new Set([
30117
30409
  "assign",
30118
30410
  "defineProperties",
@@ -30186,7 +30478,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30186
30478
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30187
30479
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30188
30480
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30189
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30190
30481
  }
30191
30482
  }
30192
30483
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30225,6 +30516,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30225
30516
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30226
30517
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30227
30518
  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;
30228
30520
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30229
30521
  });
30230
30522
  return mutations;
@@ -30457,7 +30749,7 @@ const noNestedComponentDefinition = defineRule({
30457
30749
  id: "no-nested-component-definition",
30458
30750
  title: "Component defined inside another component",
30459
30751
  tags: ["test-noise", "react-jsx-only"],
30460
- severity: "error",
30752
+ severity: "warn",
30461
30753
  category: "Correctness",
30462
30754
  recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
30463
30755
  create: (context) => {
@@ -31936,40 +32228,6 @@ const noPreventDefault = defineRule({
31936
32228
  }
31937
32229
  });
31938
32230
  //#endregion
31939
- //#region src/plugin/utils/is-result-discarded-call.ts
31940
- const isResultDiscardedCall = (callExpression) => {
31941
- let node = callExpression;
31942
- let parent = node.parent;
31943
- while (parent) {
31944
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
31945
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31946
- if (isNodeOfType(parent, "ChainExpression")) {
31947
- node = parent;
31948
- parent = node.parent;
31949
- continue;
31950
- }
31951
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31952
- node = parent;
31953
- parent = node.parent;
31954
- continue;
31955
- }
31956
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31957
- node = parent;
31958
- parent = node.parent;
31959
- continue;
31960
- }
31961
- if (isNodeOfType(parent, "SequenceExpression")) {
31962
- const expressions = parent.expressions ?? [];
31963
- if (expressions[expressions.length - 1] !== node) return true;
31964
- node = parent;
31965
- parent = node.parent;
31966
- continue;
31967
- }
31968
- return false;
31969
- }
31970
- return false;
31971
- };
31972
- //#endregion
31973
32231
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
31974
32232
  const isRefLatchGuardedEffect = (callbackBody) => {
31975
32233
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -36575,8 +36833,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36575
36833
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36576
36834
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36577
36835
  const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
36578
- const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
36579
- const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
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.";
36580
36837
  const DEFAULT_REACT_HOCS = [
36581
36838
  "memo",
36582
36839
  "forwardRef",
@@ -36647,6 +36904,20 @@ const isReactComponentInitializer = (expression, state) => {
36647
36904
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36648
36905
  return false;
36649
36906
  };
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
+ };
36650
36921
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36651
36922
  if (initializer) {
36652
36923
  const expression = skipTsExpression(initializer);
@@ -36677,6 +36948,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36677
36948
  reportNode
36678
36949
  };
36679
36950
  }
36951
+ if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
36952
+ kind: "namespace-object",
36953
+ reportNode
36954
+ };
36680
36955
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36681
36956
  kind: "non-component",
36682
36957
  reportNode
@@ -36693,7 +36968,7 @@ const collectRelevantNodes = (programRoot) => {
36693
36968
  walkAst(programRoot, (child) => {
36694
36969
  const childType = child.type;
36695
36970
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36696
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
36971
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36697
36972
  });
36698
36973
  return {
36699
36974
  exportNodes,
@@ -36738,18 +37013,31 @@ const onlyExportComponents = defineRule({
36738
37013
  category: "Architecture",
36739
37014
  create: (context) => {
36740
37015
  const settings = resolveSettings$6(context.settings);
36741
- const state = {
36742
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36743
- allowExportNames: new Set(settings.allowExportNames),
36744
- allowConstantExport: settings.allowConstantExport
36745
- };
36746
37016
  return { Program(node) {
36747
37017
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
36748
37018
  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
+ }
36749
37038
  const exports = [];
36750
37039
  let hasReactExport = false;
36751
37040
  let hasAnyExports = false;
36752
- const localComponents = [];
36753
37041
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
36754
37042
  for (const child of exportNodes) {
36755
37043
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -36820,7 +37108,14 @@ const onlyExportComponents = defineRule({
36820
37108
  });
36821
37109
  continue;
36822
37110
  }
36823
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
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")) {
36824
37119
  context.report({
36825
37120
  node: stripped,
36826
37121
  message: ANONYMOUS_MESSAGE
@@ -36873,32 +37168,23 @@ const onlyExportComponents = defineRule({
36873
37168
  let entry;
36874
37169
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
36875
37170
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
36876
- else entry = {
36877
- kind: "non-component",
36878
- reportNode
36879
- };
37171
+ else {
37172
+ entry = {
37173
+ kind: "non-component",
37174
+ reportNode
37175
+ };
37176
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
37177
+ }
36880
37178
  if (isReExportFromSource && entry.kind !== "react-component") continue;
36881
37179
  exports.push(entry);
36882
37180
  }
36883
37181
  }
36884
37182
  }
36885
37183
  for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
36886
- const isInsideExport = (node) => {
36887
- let walker = node.parent;
36888
- while (walker) {
36889
- if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
36890
- walker = walker.parent ?? null;
36891
- }
36892
- return false;
36893
- };
36894
- for (const child of componentCandidates) {
36895
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36896
- if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36897
- }
36898
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36899
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36900
- }
36901
- }
37184
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
37185
+ node: entry.reportNode,
37186
+ message: NAMESPACE_OBJECT_MESSAGE
37187
+ });
36902
37188
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
36903
37189
  if (entry.kind === "non-component") context.report({
36904
37190
  node: entry.reportNode,
@@ -36909,14 +37195,6 @@ const onlyExportComponents = defineRule({
36909
37195
  message: REACT_CONTEXT_MESSAGE
36910
37196
  });
36911
37197
  }
36912
- else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
36913
- node: local,
36914
- message: LOCAL_COMPONENT_MESSAGE
36915
- });
36916
- else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
36917
- node: local,
36918
- message: NO_EXPORT_MESSAGE
36919
- });
36920
37198
  } };
36921
37199
  }
36922
37200
  });
@@ -37714,8 +37992,8 @@ const preferHtmlDialog = defineRule({
37714
37992
  if (!HTML_TAGS.has(tagName)) return;
37715
37993
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37716
37994
  if (roleAttribute) {
37717
- const roleValue = getJsxPropStringValue(roleAttribute);
37718
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
37995
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
37996
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37719
37997
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37720
37998
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37721
37999
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -40704,6 +40982,7 @@ const rerenderLazyRefInit = defineRule({
40704
40982
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40705
40983
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40706
40984
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40985
+ if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
40707
40986
  if (isPlainCall && isReactHookName(calleeName)) return;
40708
40987
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40709
40988
  context.report({
@@ -40736,18 +41015,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40736
41015
  "getUTCMilliseconds",
40737
41016
  "valueOf"
40738
41017
  ]);
40739
- const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
40740
- "Date",
40741
- "Map",
40742
- "Set",
40743
- "WeakMap",
40744
- "WeakSet",
40745
- "RegExp",
40746
- "Error",
40747
- "URL",
40748
- "URLSearchParams",
40749
- "AbortController"
40750
- ]);
40751
41018
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40752
41019
  const findEagerInitializerCall = (expression, depth = 0) => {
40753
41020
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
@@ -41491,7 +41758,7 @@ const rnBottomSheetPreferNative = defineRule({
41491
41758
  });
41492
41759
  //#endregion
41493
41760
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41494
- const EMPTY_VISITORS$4 = {};
41761
+ const EMPTY_VISITORS$5 = {};
41495
41762
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41496
41763
  const DETOX_ELEMENT_ACTIONS = new Set([
41497
41764
  "tap",
@@ -41551,7 +41818,7 @@ const rnDetoxMissingAwait = defineRule({
41551
41818
  recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
41552
41819
  create: (context) => {
41553
41820
  const filename = normalizeFilename(context.filename ?? "");
41554
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
41821
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41555
41822
  return { ExpressionStatement(node) {
41556
41823
  const expression = node.expression;
41557
41824
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -42526,7 +42793,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42526
42793
  };
42527
42794
  //#endregion
42528
42795
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42529
- const EMPTY_VISITORS$3 = {};
42796
+ const EMPTY_VISITORS$4 = {};
42530
42797
  const reportLegacyShadowProperties = (objectExpression, context) => {
42531
42798
  const legacyShadowPropertyNames = [];
42532
42799
  for (const property of objectExpression.properties ?? []) {
@@ -42549,7 +42816,7 @@ const rnNoLegacyShadowStyles = defineRule({
42549
42816
  severity: "warn",
42550
42817
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42551
42818
  create: (context) => {
42552
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
42819
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42553
42820
  return {
42554
42821
  JSXAttribute(node) {
42555
42822
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -43470,7 +43737,7 @@ const isExpoManagedFileActive = (context) => {
43470
43737
  };
43471
43738
  //#endregion
43472
43739
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43473
- const EMPTY_VISITORS$2 = {};
43740
+ const EMPTY_VISITORS$3 = {};
43474
43741
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43475
43742
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43476
43743
  const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
@@ -43504,7 +43771,7 @@ const rnPreferExpoImage = defineRule({
43504
43771
  severity: "warn",
43505
43772
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43506
43773
  create: (context) => {
43507
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
43774
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43508
43775
  const flaggedImports = [];
43509
43776
  const assetBindingNames = /* @__PURE__ */ new Set();
43510
43777
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -43991,7 +44258,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
43991
44258
  });
43992
44259
  //#endregion
43993
44260
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
43994
- const EMPTY_VISITORS$1 = {};
44261
+ const EMPTY_VISITORS$2 = {};
43995
44262
  const IOS_SHADOW_KEYS = new Set([
43996
44263
  "shadowColor",
43997
44264
  "shadowOffset",
@@ -44077,7 +44344,7 @@ const rnStylePreferBoxShadow = defineRule({
44077
44344
  severity: "warn",
44078
44345
  recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
44079
44346
  create: (context) => {
44080
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
44347
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44081
44348
  return {
44082
44349
  JSXAttribute(node) {
44083
44350
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44174,9 +44441,9 @@ const roleHasRequiredAriaProps = defineRule({
44174
44441
  if (!HTML_TAGS.has(elementType)) return;
44175
44442
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44176
44443
  if (!roleAttribute) return;
44177
- const roleValue = getJsxPropStringValue(roleAttribute);
44178
- if (roleValue === null) return;
44179
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
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)));
44180
44447
  for (const role of roles) {
44181
44448
  const required = ROLE_REQUIRED_PROPS.get(role);
44182
44449
  if (!required) continue;
@@ -47293,7 +47560,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47293
47560
  };
47294
47561
  //#endregion
47295
47562
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47296
- const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
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
+ };
47297
47566
  const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
47298
47567
  const roleSupportsAriaProps = defineRule({
47299
47568
  id: "role-supports-aria-props",
@@ -47321,17 +47590,21 @@ const roleSupportsAriaProps = defineRule({
47321
47590
  if (!ariaAttributes) return;
47322
47591
  const elementType = getElementType(node, context.settings);
47323
47592
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47324
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47325
- if (!role) return;
47326
- if (!VALID_ARIA_ROLES.has(role)) return;
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
+ }
47327
47602
  const isImplicit = !roleAttribute;
47328
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47329
- if (!supported) return;
47330
47603
  for (const { attribute, propName } of ariaAttributes) {
47331
- if (supported.has(propName)) continue;
47604
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47332
47605
  context.report({
47333
47606
  node: attribute,
47334
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
47607
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47335
47608
  });
47336
47609
  }
47337
47610
  } })
@@ -47683,11 +47956,15 @@ const isUseEffectEventSymbol = (symbol) => {
47683
47956
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47684
47957
  return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47685
47958
  };
47686
- const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47687
- const isNonReactEffectEventSymbol = (symbol, contextNode) => {
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) => {
47688
47965
  const initializer = symbol.initializer;
47689
47966
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47690
- return isNonReactEffectEventCallee(initializer.callee, contextNode);
47967
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47691
47968
  };
47692
47969
  const findEnclosingComponentOrHookFunction = (node) => {
47693
47970
  let current = node.parent;
@@ -47766,7 +48043,7 @@ const rulesOfHooks = defineRule({
47766
48043
  const hookContext = isHookCall$1(node, context.scopes, settings);
47767
48044
  if (!hookContext) return;
47768
48045
  const { hookName } = hookContext;
47769
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
48046
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47770
48047
  context.report({
47771
48048
  node: node.callee,
47772
48049
  message: buildEffectEventPassedDownMessage()
@@ -47879,7 +48156,7 @@ const rulesOfHooks = defineRule({
47879
48156
  Identifier(node) {
47880
48157
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47881
48158
  if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47882
- if (isNonReactEffectEventSymbol(symbol, node)) return;
48159
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
47883
48160
  if (!isSameComponentOrHookScope(symbol, node)) return;
47884
48161
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47885
48162
  context.report({
@@ -55451,6 +55728,34 @@ const reactDoctorRules = [
55451
55728
  ];
55452
55729
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55453
55730
  //#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
55454
55759
  //#region src/plugin/utils/wrap-react-native-rule.ts
55455
55760
  const EMPTY_VISITORS = {};
55456
55761
  const wrapReactNativeRule = (rule) => {
@@ -55920,9 +56225,14 @@ const wrapWithSemanticContext = (rule) => ({
55920
56225
  });
55921
56226
  //#endregion
55922
56227
  //#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
+ };
55923
56233
  const applyFrameworkRuleWrappers = (registry) => {
55924
56234
  const wrapped = {};
55925
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
56235
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55926
56236
  return wrapped;
55927
56237
  };
55928
56238
  const plugin = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.2-dev.43d766f",
3
+ "version": "0.7.2-dev.82e0475",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",