oxlint-plugin-react-doctor 0.7.2-dev.aa519e5 → 0.7.2-dev.cb8f726

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +2350 -1563
  2. package/dist/index.js +324 -162
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -595,6 +595,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
595
595
  "parseInt",
596
596
  "parseFloat"
597
597
  ]);
598
+ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
599
+ "Date",
600
+ "Map",
601
+ "Set",
602
+ "WeakMap",
603
+ "WeakSet",
604
+ "WeakRef",
605
+ "RegExp",
606
+ "Error",
607
+ "URL",
608
+ "URLSearchParams",
609
+ "AbortController"
610
+ ]);
598
611
  const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
599
612
  "Boolean",
600
613
  "String",
@@ -2235,6 +2248,38 @@ const anchorHasContent = defineRule({
2235
2248
  }
2236
2249
  });
2237
2250
  //#endregion
2251
+ //#region src/plugin/utils/get-jsx-prop-static-string-values.ts
2252
+ const MAX_CONST_RESOLUTION_HOPS = 4;
2253
+ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2254
+ const expression = stripParenExpression(rawExpression);
2255
+ if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
2256
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2257
+ const staticValue = getStaticTemplateLiteralValue(expression);
2258
+ return staticValue === null ? null : [staticValue];
2259
+ }
2260
+ if (isNodeOfType(expression, "ConditionalExpression")) {
2261
+ const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
2262
+ if (consequentValues === null) return null;
2263
+ const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
2264
+ if (alternateValues === null) return null;
2265
+ return [...consequentValues, ...alternateValues];
2266
+ }
2267
+ if (isNodeOfType(expression, "Identifier")) {
2268
+ if (remainingHops === 0) return null;
2269
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2270
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2271
+ return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2272
+ }
2273
+ return null;
2274
+ };
2275
+ const getJsxPropStaticStringValues = (attribute, scopes) => {
2276
+ const value = attribute.value;
2277
+ if (!value) return null;
2278
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
2279
+ if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
2280
+ return null;
2281
+ };
2282
+ //#endregion
2238
2283
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2239
2284
  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
2285
  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 +2309,11 @@ const isInvalidHref = (value, validHrefs) => {
2264
2309
  if (validHrefs.has(value)) return false;
2265
2310
  return value === "" || value === "#" || value === "javascript:void(0)";
2266
2311
  };
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;
2312
+ const isNullishOrFragmentHref = (value) => {
2278
2313
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2279
2314
  const expression = value.expression;
2280
2315
  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
- }
2316
+ if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2289
2317
  }
2290
2318
  if (isNodeOfType(value, "JSXFragment")) return true;
2291
2319
  return false;
@@ -2318,9 +2346,10 @@ const anchorIsValid = defineRule({
2318
2346
  });
2319
2347
  return;
2320
2348
  }
2321
- if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2349
+ const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2350
+ if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2322
2351
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2323
- if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2352
+ if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2324
2353
  context.report({
2325
2354
  node: node.name,
2326
2355
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3376,6 +3405,25 @@ const ariaRole = defineRule({
3376
3405
  if (!roleAttribute) return;
3377
3406
  const elementType = getElementType(node, context.settings);
3378
3407
  if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
3408
+ const reportFirstInvalidCandidate = (candidates) => {
3409
+ for (const candidate of candidates) {
3410
+ if (candidate.trim().length === 0) {
3411
+ context.report({
3412
+ node: roleAttribute,
3413
+ message: buildBaseMessage("")
3414
+ });
3415
+ return;
3416
+ }
3417
+ const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
3418
+ for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3419
+ context.report({
3420
+ node: roleAttribute,
3421
+ message: buildBaseMessage(` \`${token}\` is not one.`)
3422
+ });
3423
+ return;
3424
+ }
3425
+ }
3426
+ };
3379
3427
  const value = roleAttribute.value;
3380
3428
  if (!value) {
3381
3429
  context.report({
@@ -3392,22 +3440,7 @@ const ariaRole = defineRule({
3392
3440
  });
3393
3441
  return;
3394
3442
  }
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
- }
3443
+ reportFirstInvalidCandidate([value.value]);
3411
3444
  return;
3412
3445
  }
3413
3446
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3428,6 +3461,11 @@ const ariaRole = defineRule({
3428
3461
  });
3429
3462
  return;
3430
3463
  }
3464
+ const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3465
+ if (resolvedCandidates !== null) {
3466
+ reportFirstInvalidCandidate(resolvedCandidates);
3467
+ return;
3468
+ }
3431
3469
  return;
3432
3470
  }
3433
3471
  context.report({
@@ -9616,7 +9654,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9616
9654
  });
9617
9655
  //#endregion
9618
9656
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9619
- const EMPTY_VISITORS$5 = {};
9657
+ const EMPTY_VISITORS$6 = {};
9620
9658
  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
9659
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9622
9660
  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 +9666,7 @@ const expoNoNonInlinedEnv = defineRule({
9628
9666
  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
9667
  create: (context) => {
9630
9668
  const filename = normalizeFilename(context.filename ?? "");
9631
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9669
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9632
9670
  return {
9633
9671
  MemberExpression(node) {
9634
9672
  if (!node.computed) return;
@@ -11046,8 +11084,8 @@ const interactiveSupportsFocus = defineRule({
11046
11084
  if (node.attributes.length === 0) return;
11047
11085
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11048
11086
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11049
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
- if (!role) return;
11087
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11088
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11051
11089
  let hasInteractiveHandler = false;
11052
11090
  for (const attribute of node.attributes) {
11053
11091
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11061,11 +11099,16 @@ const interactiveSupportsFocus = defineRule({
11061
11099
  const elementType = getElementType(node, context.settings);
11062
11100
  if (!HTML_TAGS.has(elementType)) return;
11063
11101
  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
11102
  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);
11103
+ const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11104
+ for (const role of roleCandidates) {
11105
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11106
+ if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11107
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11108
+ }
11109
+ const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11110
+ const roleDisplay = roleCandidates.join("' / '");
11111
+ const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11069
11112
  context.report({
11070
11113
  node,
11071
11114
  message
@@ -13508,7 +13551,7 @@ const jsTosortedImmutable = defineRule({
13508
13551
  title: "Spread copy before sort()",
13509
13552
  tags: ["test-noise"],
13510
13553
  severity: "warn",
13511
- disabledBy: ["react-native", "pre-es2023"],
13554
+ disabledWhen: ["react-native", "pre-es2023"],
13512
13555
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
13513
13556
  create: (context) => ({ CallExpression(node) {
13514
13557
  if (!isMemberProperty(node.callee, "sort")) return;
@@ -14755,7 +14798,7 @@ const jsxNoConstructedContextValues = defineRule({
14755
14798
  title: "Unstable context provider value",
14756
14799
  tags: ["react-jsx-only"],
14757
14800
  severity: "warn",
14758
- disabledBy: ["react-compiler"],
14801
+ disabledWhen: ["react-compiler"],
14759
14802
  recommendation: "Wrap the context value in `useMemo` or move it outside the component so consumers do not redraw every render.",
14760
14803
  category: "Performance",
14761
14804
  create: (context) => {
@@ -15123,7 +15166,7 @@ const jsxNoJsxAsProp = defineRule({
15123
15166
  title: "JSX element passed as a prop",
15124
15167
  tags: ["react-jsx-only"],
15125
15168
  severity: "warn",
15126
- disabledBy: ["react-compiler"],
15169
+ disabledWhen: ["react-compiler"],
15127
15170
  recommendation: "Move the JSX outside the component or wrap it in `useMemo` so memoized children do not redraw every render.",
15128
15171
  category: "Performance",
15129
15172
  create: (context) => {
@@ -15511,7 +15554,7 @@ const jsxNoNewArrayAsProp = defineRule({
15511
15554
  title: "New array passed as a prop",
15512
15555
  tags: ["react-jsx-only"],
15513
15556
  severity: "warn",
15514
- disabledBy: ["react-compiler"],
15557
+ disabledWhen: ["react-compiler"],
15515
15558
  recommendation: "Wrap the array in `useMemo` or move it outside the component so memoized children do not redraw every render.",
15516
15559
  category: "Performance",
15517
15560
  create: (context) => {
@@ -15975,7 +16018,7 @@ const jsxNoNewFunctionAsProp = defineRule({
15975
16018
  title: "New function passed as a prop",
15976
16019
  tags: ["react-jsx-only"],
15977
16020
  severity: "warn",
15978
- disabledBy: ["react-compiler"],
16021
+ disabledWhen: ["react-compiler"],
15979
16022
  recommendation: "Wrap the callback in `useCallback` or move it outside the component so memoized children do not redraw every render.",
15980
16023
  category: "Performance",
15981
16024
  create: (context) => {
@@ -16282,7 +16325,7 @@ const jsxNoNewObjectAsProp = defineRule({
16282
16325
  title: "New object passed as a prop",
16283
16326
  tags: ["react-jsx-only"],
16284
16327
  severity: "warn",
16285
- disabledBy: ["react-compiler"],
16328
+ disabledWhen: ["react-compiler"],
16286
16329
  recommendation: "Wrap the object in `useMemo` or move it outside the component so memoized children do not redraw every render.",
16287
16330
  category: "Performance",
16288
16331
  create: (context) => {
@@ -18106,6 +18149,7 @@ const getReactDoctorStringArraySetting = (settings, settingName) => {
18106
18149
  if (!Array.isArray(settingValue)) return [];
18107
18150
  return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
18108
18151
  };
18152
+ const hasCapability = (settings, capability) => getReactDoctorStringArraySetting(settings, "capabilities").includes(capability);
18109
18153
  //#endregion
18110
18154
  //#region src/plugin/utils/is-in-project-directory.ts
18111
18155
  const isInProjectDirectory = (context, directoryPath) => {
@@ -18560,6 +18604,7 @@ const describeClientSideNavigation = (node) => {
18560
18604
  }
18561
18605
  return null;
18562
18606
  };
18607
+ const STATIC_EXPORT_RECOMMENDATION = "Avoid redirects inside useEffect — they flash the wrong page first. Use an event handler (e.g. onClick), or call redirect() from next/navigation during render (it prerenders a client-side redirect under output: \"export\"). Middleware and getServerSideProps redirects aren't available in a static export.";
18563
18608
  const nextjsNoClientSideRedirect = defineRule({
18564
18609
  id: "nextjs-no-client-side-redirect",
18565
18610
  title: "Client-side redirect for navigation",
@@ -18567,6 +18612,7 @@ const nextjsNoClientSideRedirect = defineRule({
18567
18612
  requires: ["nextjs"],
18568
18613
  severity: "warn",
18569
18614
  recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
18615
+ recommendationFor: (hasCapability) => hasCapability("nextjs:static-export") ? STATIC_EXPORT_RECOMMENDATION : void 0,
18570
18616
  create: (context) => {
18571
18617
  return { CallExpression(node) {
18572
18618
  if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
@@ -22474,6 +22520,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22474
22520
  const section = manifest[sectionName];
22475
22521
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22476
22522
  });
22523
+ const declaresDependency = (manifest, dependencyName) => {
22524
+ for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22525
+ return false;
22526
+ };
22477
22527
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22478
22528
  const classifyPackagePlatform = (filename) => {
22479
22529
  const manifest = readNearestPackageManifest(filename);
@@ -22490,9 +22540,7 @@ const classifyPackagePlatform = (filename) => {
22490
22540
  return result;
22491
22541
  };
22492
22542
  //#endregion
22493
- //#region src/plugin/utils/is-react-native-file.ts
22494
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22495
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22543
+ //#region src/plugin/utils/is-package-nested-below-project-root.ts
22496
22544
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22497
22545
  const resolveRealDirectory = (directory) => {
22498
22546
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22513,6 +22561,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22513
22561
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22514
22562
  return realPackageDirectory.startsWith(rootPrefix);
22515
22563
  };
22564
+ //#endregion
22565
+ //#region src/plugin/utils/is-react-native-file.ts
22566
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22567
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22516
22568
  const classifyReactNativeFileTarget = (context) => {
22517
22569
  const rawFilename = context.filename;
22518
22570
  if (!rawFilename) return "unknown";
@@ -22929,7 +22981,6 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
22929
22981
  let fallThroughCount = 0;
22930
22982
  let maxTerminatingPathCount = 0;
22931
22983
  for (const statement of statements) {
22932
- if (isFunctionLike$1(statement)) continue;
22933
22984
  const guardBranch = isGuardWithTerminatingBranch(statement);
22934
22985
  if (guardBranch) {
22935
22986
  maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
@@ -22965,14 +23016,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
22965
23016
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
22966
23017
  return parent.parent === effectCallback.body;
22967
23018
  };
23019
+ const countFunctionBodySetStateCalls = (functionNode, context) => {
23020
+ if (isAsyncFunctionLike(functionNode)) return 0;
23021
+ const body = functionNode.body;
23022
+ if (!body) return 0;
23023
+ return countMaxPathSetStateCalls(body, context);
23024
+ };
22968
23025
  const countMaxPathSetStateCalls = (node, context) => {
22969
23026
  if (!node || typeof node !== "object") return 0;
22970
- if (isAsyncFunctionLike(node)) return 0;
23027
+ if (isFunctionLike$1(node)) return 0;
22971
23028
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
22972
23029
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
22973
23030
  const consequent = node.consequent;
22974
23031
  const alternate = node.alternate;
22975
- return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
23032
+ const testCount = countMaxPathSetStateCalls(node.test, context);
23033
+ const thenCount = countMaxPathSetStateCalls(consequent, context);
23034
+ const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
23035
+ return testCount + Math.max(thenCount, elseCount);
22976
23036
  }
22977
23037
  if (isNodeOfType(node, "SwitchStatement")) {
22978
23038
  let maxRunSetters = 0;
@@ -23002,28 +23062,31 @@ const countMaxPathSetStateCalls = (node, context) => {
23002
23062
  }
23003
23063
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23004
23064
  let nestedSettersInArgs = 0;
23005
- for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23065
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23006
23066
  return 1 + nestedSettersInArgs;
23007
23067
  }
23008
23068
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23009
23069
  const helperFunction = context.helpersByName.get(node.callee.name);
23010
23070
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23011
23071
  context.activeHelpers.add(helperFunction);
23012
- let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23072
+ let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23013
23073
  context.activeHelpers.delete(helperFunction);
23014
23074
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23015
23075
  return helperCount;
23016
23076
  }
23017
23077
  }
23018
- const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23078
+ const countChild = (child) => {
23079
+ if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23080
+ return countMaxPathSetStateCalls(child, context);
23081
+ };
23019
23082
  let total = 0;
23020
23083
  const nodeRecord = node;
23021
23084
  for (const key of Object.keys(nodeRecord)) {
23022
23085
  if (key === "parent") continue;
23023
23086
  const child = nodeRecord[key];
23024
23087
  if (Array.isArray(child)) {
23025
- for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
23026
- } else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
23088
+ for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
23089
+ } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
23027
23090
  }
23028
23091
  return total;
23029
23092
  };
@@ -23072,7 +23135,7 @@ const noCascadingSetState = defineRule({
23072
23135
  const callback = getEffectCallback(node);
23073
23136
  if (!callback) return;
23074
23137
  if (isDevOnlyGuardedEffect(callback)) return;
23075
- const setStateCallCount = countMaxPathSetStateCalls(callback, {
23138
+ const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23076
23139
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23077
23140
  activeHelpers: /* @__PURE__ */ new Set(),
23078
23141
  effectCallback: callback
@@ -26290,7 +26353,11 @@ const noEffectEventInDeps = defineRule({
26290
26353
  const initializer = declaratorNode.init;
26291
26354
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26292
26355
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26293
- if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26356
+ if (isNodeOfType(initializer.callee, "Identifier")) {
26357
+ if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26358
+ const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26359
+ if (calleeSymbol && calleeSymbol.kind !== "import") return;
26360
+ }
26294
26361
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26295
26362
  } });
26296
26363
  return {
@@ -28928,7 +28995,7 @@ const noJsonParseStringifyClone = defineRule({
28928
28995
  id: "no-json-parse-stringify-clone",
28929
28996
  title: "JSON parse/stringify deep clone",
28930
28997
  severity: "warn",
28931
- disabledBy: ["react-native"],
28998
+ disabledWhen: ["react-native"],
28932
28999
  recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
28933
29000
  create: (context) => ({ CallExpression(node) {
28934
29001
  if (!isJsonMethodCall(node, "parse")) return;
@@ -31752,12 +31819,8 @@ const noPolymorphicChildren = defineRule({
31752
31819
  //#endregion
31753
31820
  //#region src/plugin/rules/correctness/no-prevent-default.ts
31754
31821
  const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
31755
- const SERVER_CAPABLE_FRAMEWORKS = new Set([
31756
- "nextjs",
31757
- "tanstack-start",
31758
- "remix"
31759
- ]);
31760
31822
  const FORM_MESSAGE_SERVER_CAPABLE = "Your users can't submit this <form> without JavaScript because onSubmit calls preventDefault(), so use a server action like `<form action={serverAction}>` to make it work either way.";
31823
+ const FORM_MESSAGE_GENERIC = "Your users can't submit this <form> because onSubmit calls preventDefault().";
31761
31824
  const ANCHOR_MESSAGE = "Your users click this <a> & nothing navigates because onClick calls preventDefault(), so use a <button> or a routing component instead.";
31762
31825
  const collectPreventDefaultCalls = (node) => {
31763
31826
  const preventDefaultCalls = [];
@@ -31894,15 +31957,18 @@ const noPreventDefault = defineRule({
31894
31957
  recommendation: "Use `<form action>` where your framework supports it (it works without JS), or use a `<button>` instead of an `<a>` with preventDefault.",
31895
31958
  create: (context) => {
31896
31959
  const framework = getReactDoctorStringSetting(context.settings, "framework");
31897
- const isServerCapableFramework = framework !== void 0 && SERVER_CAPABLE_FRAMEWORKS.has(framework);
31960
+ const isClientOnlyFramework = hasCapability(context.settings, "client-only");
31961
+ const isServerActionsFramework = hasCapability(context.settings, "server-actions");
31962
+ const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
31898
31963
  return { JSXOpeningElement(node) {
31899
31964
  const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
31900
31965
  if (!elementName) return;
31901
31966
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
31902
31967
  if (!targetEventProps) return;
31903
31968
  if (elementName === "form") {
31904
- if (!isServerCapableFramework) return;
31905
- if (framework === "nextjs") {
31969
+ if (isClientOnlyFramework) return;
31970
+ if (framework === void 0 || framework === "unknown") return;
31971
+ if (framework === "nextjs" && isServerActionsFramework) {
31906
31972
  const programRoot = findProgramRoot(node);
31907
31973
  if (!programRoot || !hasDirective(programRoot, "use client")) return;
31908
31974
  }
@@ -31926,7 +31992,7 @@ const noPreventDefault = defineRule({
31926
31992
  }
31927
31993
  context.report({
31928
31994
  node,
31929
- message: elementName === "form" ? FORM_MESSAGE_SERVER_CAPABLE : ANCHOR_MESSAGE
31995
+ message: elementName === "form" ? formMessage : ANCHOR_MESSAGE
31930
31996
  });
31931
31997
  return;
31932
31998
  }
@@ -33196,11 +33262,41 @@ const isIdentifierLikeKeyNameValue = (literalValue) => {
33196
33262
  if (wordSegments.length < 2) return false;
33197
33263
  return wordSegments.every((segment) => /^[a-z]+$/.test(segment));
33198
33264
  };
33265
+ const FRAMEWORK_ENV_ADVICE = [
33266
+ [
33267
+ "nextjs",
33268
+ "Next.js",
33269
+ "NEXT_PUBLIC_*"
33270
+ ],
33271
+ [
33272
+ "vite",
33273
+ "Vite",
33274
+ "VITE_*"
33275
+ ],
33276
+ [
33277
+ "tanstack-start",
33278
+ "TanStack Start",
33279
+ "VITE_*"
33280
+ ],
33281
+ [
33282
+ "cra",
33283
+ "Create React App",
33284
+ "REACT_APP_*"
33285
+ ],
33286
+ [
33287
+ "gatsby",
33288
+ "Gatsby",
33289
+ "GATSBY_*"
33290
+ ]
33291
+ ];
33199
33292
  const noSecretsInClientCode = defineRule({
33200
33293
  id: "no-secrets-in-client-code",
33201
33294
  title: "Secret in client code",
33202
33295
  severity: "warn",
33203
33296
  recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
33297
+ recommendationFor: (hasCapability) => {
33298
+ for (const [frameworkToken, frameworkName, publicEnvPrefix] of FRAMEWORK_ENV_ADVICE) if (hasCapability(frameworkToken)) return `Move secrets to server-only code. In ${frameworkName}, only \`${publicEnvPrefix}\` env vars are exposed to the browser, and they must not contain secrets`;
33299
+ },
33204
33300
  create: (context) => {
33205
33301
  const filename = normalizeFilename(context.filename ?? "");
33206
33302
  const framework = getReactDoctorStringSetting(context.settings, "framework");
@@ -33238,7 +33334,7 @@ const noSecretsInClientCode = defineRule({
33238
33334
  const isServerOnlyScope = isInsideServerOnlyScope(node);
33239
33335
  const trailingSuffix = getIdentifierTrailingWord(variableName);
33240
33336
  const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
33241
- if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
33337
+ if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
33242
33338
  context.report({
33243
33339
  node,
33244
33340
  message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
@@ -36543,8 +36639,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36543
36639
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36544
36640
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36545
36641
  const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
36546
- const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
36547
- const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
36642
+ 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.";
36548
36643
  const DEFAULT_REACT_HOCS = [
36549
36644
  "memo",
36550
36645
  "forwardRef",
@@ -36615,6 +36710,20 @@ const isReactComponentInitializer = (expression, state) => {
36615
36710
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36616
36711
  return false;
36617
36712
  };
36713
+ const objectExpressionBundlesComponents = (objectExpression, state) => {
36714
+ for (const property of objectExpression.properties ?? []) {
36715
+ if (!isNodeOfType(property, "Property")) continue;
36716
+ const value = skipTsExpression(property.value);
36717
+ if (isNodeOfType(value, "Identifier")) {
36718
+ if (state.localComponentNames.has(value.name)) return true;
36719
+ continue;
36720
+ }
36721
+ if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36722
+ if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
36723
+ if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36724
+ }
36725
+ return false;
36726
+ };
36618
36727
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36619
36728
  if (initializer) {
36620
36729
  const expression = skipTsExpression(initializer);
@@ -36645,6 +36754,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36645
36754
  reportNode
36646
36755
  };
36647
36756
  }
36757
+ if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
36758
+ kind: "namespace-object",
36759
+ reportNode
36760
+ };
36648
36761
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36649
36762
  kind: "non-component",
36650
36763
  reportNode
@@ -36661,7 +36774,7 @@ const collectRelevantNodes = (programRoot) => {
36661
36774
  walkAst(programRoot, (child) => {
36662
36775
  const childType = child.type;
36663
36776
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36664
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
36777
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36665
36778
  });
36666
36779
  return {
36667
36780
  exportNodes,
@@ -36706,18 +36819,31 @@ const onlyExportComponents = defineRule({
36706
36819
  category: "Architecture",
36707
36820
  create: (context) => {
36708
36821
  const settings = resolveSettings$6(context.settings);
36709
- const state = {
36710
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36711
- allowExportNames: new Set(settings.allowExportNames),
36712
- allowConstantExport: settings.allowConstantExport
36713
- };
36714
36822
  return { Program(node) {
36715
36823
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
36716
36824
  const { exportNodes, componentCandidates } = collectRelevantNodes(node);
36825
+ const localComponentNames = /* @__PURE__ */ new Set();
36826
+ const state = {
36827
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36828
+ allowExportNames: new Set(settings.allowExportNames),
36829
+ allowConstantExport: settings.allowConstantExport,
36830
+ localComponentNames
36831
+ };
36832
+ for (const child of componentCandidates) {
36833
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36834
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36835
+ }
36836
+ if (isNodeOfType(child, "ClassDeclaration") && child.id) {
36837
+ if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36838
+ }
36839
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36840
+ const initializer = child.init;
36841
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36842
+ }
36843
+ }
36717
36844
  const exports = [];
36718
36845
  let hasReactExport = false;
36719
36846
  let hasAnyExports = false;
36720
- const localComponents = [];
36721
36847
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
36722
36848
  for (const child of exportNodes) {
36723
36849
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -36788,7 +36914,14 @@ const onlyExportComponents = defineRule({
36788
36914
  });
36789
36915
  continue;
36790
36916
  }
36791
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
36917
+ if (isNodeOfType(stripped, "ObjectExpression")) {
36918
+ context.report({
36919
+ node: stripped,
36920
+ message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
36921
+ });
36922
+ continue;
36923
+ }
36924
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
36792
36925
  context.report({
36793
36926
  node: stripped,
36794
36927
  message: ANONYMOUS_MESSAGE
@@ -36841,32 +36974,23 @@ const onlyExportComponents = defineRule({
36841
36974
  let entry;
36842
36975
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
36843
36976
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
36844
- else entry = {
36845
- kind: "non-component",
36846
- reportNode
36847
- };
36977
+ else {
36978
+ entry = {
36979
+ kind: "non-component",
36980
+ reportNode
36981
+ };
36982
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
36983
+ }
36848
36984
  if (isReExportFromSource && entry.kind !== "react-component") continue;
36849
36985
  exports.push(entry);
36850
36986
  }
36851
36987
  }
36852
36988
  }
36853
36989
  for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
36854
- const isInsideExport = (node) => {
36855
- let walker = node.parent;
36856
- while (walker) {
36857
- if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
36858
- walker = walker.parent ?? null;
36859
- }
36860
- return false;
36861
- };
36862
- for (const child of componentCandidates) {
36863
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36864
- if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36865
- }
36866
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36867
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36868
- }
36869
- }
36990
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
36991
+ node: entry.reportNode,
36992
+ message: NAMESPACE_OBJECT_MESSAGE
36993
+ });
36870
36994
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
36871
36995
  if (entry.kind === "non-component") context.report({
36872
36996
  node: entry.reportNode,
@@ -36877,14 +37001,6 @@ const onlyExportComponents = defineRule({
36877
37001
  message: REACT_CONTEXT_MESSAGE
36878
37002
  });
36879
37003
  }
36880
- else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
36881
- node: local,
36882
- message: LOCAL_COMPONENT_MESSAGE
36883
- });
36884
- else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
36885
- node: local,
36886
- message: NO_EXPORT_MESSAGE
36887
- });
36888
37004
  } };
36889
37005
  }
36890
37006
  });
@@ -37682,8 +37798,8 @@ const preferHtmlDialog = defineRule({
37682
37798
  if (!HTML_TAGS.has(tagName)) return;
37683
37799
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37684
37800
  if (roleAttribute) {
37685
- const roleValue = getJsxPropStringValue(roleAttribute);
37686
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
37801
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
37802
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37687
37803
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37688
37804
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37689
37805
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -37962,7 +38078,7 @@ const preferModuleScopeStaticValue = defineRule({
37962
38078
  tags: ["test-noise"],
37963
38079
  severity: "warn",
37964
38080
  category: "Architecture",
37965
- disabledBy: ["react-compiler"],
38081
+ disabledWhen: ["react-compiler"],
37966
38082
  recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
37967
38083
  create: (context) => ({ VariableDeclarator(node) {
37968
38084
  if (!isNodeOfType(node.id, "Identifier")) return;
@@ -38032,7 +38148,7 @@ const preferStableEmptyFallback = defineRule({
38032
38148
  tags: ["react-jsx-only", "test-noise"],
38033
38149
  severity: "warn",
38034
38150
  category: "Performance",
38035
- disabledBy: ["react-compiler"],
38151
+ disabledWhen: ["react-compiler"],
38036
38152
  recommendation: "Make a `const EMPTY = []` (or `{}`) at module scope and use it as the `||` / `??` fallback, so the value stays the same each render.",
38037
38153
  create: (context) => {
38038
38154
  let memoRegistry = null;
@@ -39361,7 +39477,7 @@ const reduxUseselectorInlineDerivation = defineRule({
39361
39477
  title: "useSelector derives data inline",
39362
39478
  severity: "warn",
39363
39479
  category: "Performance",
39364
- disabledBy: ["react-compiler"],
39480
+ disabledWhen: ["react-compiler"],
39365
39481
  recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
39366
39482
  create: (context) => {
39367
39483
  let aliases = /* @__PURE__ */ new Set();
@@ -39421,7 +39537,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
39421
39537
  title: "useSelector returns a new collection",
39422
39538
  severity: "warn",
39423
39539
  category: "Performance",
39424
- disabledBy: ["react-compiler"],
39540
+ disabledWhen: ["react-compiler"],
39425
39541
  recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
39426
39542
  create: (context) => {
39427
39543
  let aliases = /* @__PURE__ */ new Set();
@@ -39721,7 +39837,7 @@ const renderingHydrationMismatchTime = defineRule({
39721
39837
  title: "Time or random value in JSX",
39722
39838
  severity: "warn",
39723
39839
  category: "Correctness",
39724
- disabledBy: ["vite", "cra"],
39840
+ disabledWhen: ["vite", "cra"],
39725
39841
  recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
39726
39842
  create: (context) => {
39727
39843
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -40672,6 +40788,7 @@ const rerenderLazyRefInit = defineRule({
40672
40788
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40673
40789
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40674
40790
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40791
+ if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
40675
40792
  if (isPlainCall && isReactHookName(calleeName)) return;
40676
40793
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40677
40794
  context.report({
@@ -40704,18 +40821,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40704
40821
  "getUTCMilliseconds",
40705
40822
  "valueOf"
40706
40823
  ]);
40707
- const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
40708
- "Date",
40709
- "Map",
40710
- "Set",
40711
- "WeakMap",
40712
- "WeakSet",
40713
- "RegExp",
40714
- "Error",
40715
- "URL",
40716
- "URLSearchParams",
40717
- "AbortController"
40718
- ]);
40719
40824
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40720
40825
  const findEagerInitializerCall = (expression, depth = 0) => {
40721
40826
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
@@ -41459,7 +41564,7 @@ const rnBottomSheetPreferNative = defineRule({
41459
41564
  });
41460
41565
  //#endregion
41461
41566
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41462
- const EMPTY_VISITORS$4 = {};
41567
+ const EMPTY_VISITORS$5 = {};
41463
41568
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41464
41569
  const DETOX_ELEMENT_ACTIONS = new Set([
41465
41570
  "tap",
@@ -41519,7 +41624,7 @@ const rnDetoxMissingAwait = defineRule({
41519
41624
  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.",
41520
41625
  create: (context) => {
41521
41626
  const filename = normalizeFilename(context.filename ?? "");
41522
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
41627
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41523
41628
  return { ExpressionStatement(node) {
41524
41629
  const expression = node.expression;
41525
41630
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -41688,7 +41793,7 @@ const rnListCallbackPerRow = defineRule({
41688
41793
  tags: ["test-noise"],
41689
41794
  requires: ["react-native"],
41690
41795
  severity: "warn",
41691
- disabledBy: ["react-compiler"],
41796
+ disabledWhen: ["react-compiler"],
41692
41797
  recommendation: "Move the handler out with useCallback at list scope and pass the row id as a prop. Then memo() rows skip redrawing when their data has not changed.",
41693
41798
  create: (context) => {
41694
41799
  const inspect = (node) => {
@@ -42316,7 +42421,7 @@ const rnNoInlineFlatlistRenderitem = defineRule({
42316
42421
  tags: ["test-noise"],
42317
42422
  requires: ["react-native"],
42318
42423
  severity: "warn",
42319
- disabledBy: ["react-compiler"],
42424
+ disabledWhen: ["react-compiler"],
42320
42425
  recommendation: "Move renderItem to a named function or wrap it in useCallback so it is not rebuilt every time the screen redraws.",
42321
42426
  create: (context) => ({ JSXAttribute(node) {
42322
42427
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "renderItem") return;
@@ -42341,7 +42446,7 @@ const rnNoInlineObjectInListItem = defineRule({
42341
42446
  tags: ["test-noise"],
42342
42447
  requires: ["react-native"],
42343
42448
  severity: "warn",
42344
- disabledBy: ["react-compiler"],
42449
+ disabledWhen: ["react-compiler"],
42345
42450
  recommendation: "Move style and object props out of renderItem (StyleSheet.create, useMemo at list scope, or pass primitives) so memo() rows stop redrawing when their data has not changed.",
42346
42451
  create: (context) => {
42347
42452
  const renderPropStack = [];
@@ -42494,7 +42599,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42494
42599
  };
42495
42600
  //#endregion
42496
42601
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42497
- const EMPTY_VISITORS$3 = {};
42602
+ const EMPTY_VISITORS$4 = {};
42498
42603
  const reportLegacyShadowProperties = (objectExpression, context) => {
42499
42604
  const legacyShadowPropertyNames = [];
42500
42605
  for (const property of objectExpression.properties ?? []) {
@@ -42517,7 +42622,7 @@ const rnNoLegacyShadowStyles = defineRule({
42517
42622
  severity: "warn",
42518
42623
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42519
42624
  create: (context) => {
42520
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
42625
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42521
42626
  return {
42522
42627
  JSXAttribute(node) {
42523
42628
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -43438,7 +43543,7 @@ const isExpoManagedFileActive = (context) => {
43438
43543
  };
43439
43544
  //#endregion
43440
43545
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43441
- const EMPTY_VISITORS$2 = {};
43546
+ const EMPTY_VISITORS$3 = {};
43442
43547
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43443
43548
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43444
43549
  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);
@@ -43472,7 +43577,7 @@ const rnPreferExpoImage = defineRule({
43472
43577
  severity: "warn",
43473
43578
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43474
43579
  create: (context) => {
43475
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
43580
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43476
43581
  const flaggedImports = [];
43477
43582
  const assetBindingNames = /* @__PURE__ */ new Set();
43478
43583
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -43959,7 +44064,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
43959
44064
  });
43960
44065
  //#endregion
43961
44066
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
43962
- const EMPTY_VISITORS$1 = {};
44067
+ const EMPTY_VISITORS$2 = {};
43963
44068
  const IOS_SHADOW_KEYS = new Set([
43964
44069
  "shadowColor",
43965
44070
  "shadowOffset",
@@ -44045,7 +44150,7 @@ const rnStylePreferBoxShadow = defineRule({
44045
44150
  severity: "warn",
44046
44151
  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.",
44047
44152
  create: (context) => {
44048
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
44153
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44049
44154
  return {
44050
44155
  JSXAttribute(node) {
44051
44156
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44142,9 +44247,9 @@ const roleHasRequiredAriaProps = defineRule({
44142
44247
  if (!HTML_TAGS.has(elementType)) return;
44143
44248
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44144
44249
  if (!roleAttribute) return;
44145
- const roleValue = getJsxPropStringValue(roleAttribute);
44146
- if (roleValue === null) return;
44147
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
44250
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
44251
+ if (roleCandidates === null) return;
44252
+ const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44148
44253
  for (const role of roles) {
44149
44254
  const required = ROLE_REQUIRED_PROPS.get(role);
44150
44255
  if (!required) continue;
@@ -47261,7 +47366,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47261
47366
  };
47262
47367
  //#endregion
47263
47368
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47264
- const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
47369
+ const buildMessageDefault = (roles, propName) => {
47370
+ 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.`;
47371
+ };
47265
47372
  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.`;
47266
47373
  const roleSupportsAriaProps = defineRule({
47267
47374
  id: "role-supports-aria-props",
@@ -47289,17 +47396,21 @@ const roleSupportsAriaProps = defineRule({
47289
47396
  if (!ariaAttributes) return;
47290
47397
  const elementType = getElementType(node, context.settings);
47291
47398
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47292
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47293
- if (!role) return;
47294
- if (!VALID_ARIA_ROLES.has(role)) return;
47399
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
47400
+ if (roleCandidates === null || roleCandidates.length === 0) return;
47401
+ const supportedSets = [];
47402
+ for (const role of roleCandidates) {
47403
+ if (!VALID_ARIA_ROLES.has(role)) return;
47404
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47405
+ if (!supported) return;
47406
+ supportedSets.push(supported);
47407
+ }
47295
47408
  const isImplicit = !roleAttribute;
47296
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47297
- if (!supported) return;
47298
47409
  for (const { attribute, propName } of ariaAttributes) {
47299
- if (supported.has(propName)) continue;
47410
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47300
47411
  context.report({
47301
47412
  node: attribute,
47302
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
47413
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47303
47414
  });
47304
47415
  }
47305
47416
  } })
@@ -47651,11 +47762,15 @@ const isUseEffectEventSymbol = (symbol) => {
47651
47762
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47652
47763
  return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47653
47764
  };
47654
- const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47655
- const isNonReactEffectEventSymbol = (symbol, contextNode) => {
47765
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47766
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47767
+ return Boolean(symbol && symbol.kind !== "import");
47768
+ };
47769
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47770
+ const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47656
47771
  const initializer = symbol.initializer;
47657
47772
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47658
- return isNonReactEffectEventCallee(initializer.callee, contextNode);
47773
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47659
47774
  };
47660
47775
  const findEnclosingComponentOrHookFunction = (node) => {
47661
47776
  let current = node.parent;
@@ -47734,7 +47849,7 @@ const rulesOfHooks = defineRule({
47734
47849
  const hookContext = isHookCall$1(node, context.scopes, settings);
47735
47850
  if (!hookContext) return;
47736
47851
  const { hookName } = hookContext;
47737
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
47852
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47738
47853
  context.report({
47739
47854
  node: node.callee,
47740
47855
  message: buildEffectEventPassedDownMessage()
@@ -47847,7 +47962,7 @@ const rulesOfHooks = defineRule({
47847
47962
  Identifier(node) {
47848
47963
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47849
47964
  if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47850
- if (isNonReactEffectEventSymbol(symbol, node)) return;
47965
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
47851
47966
  if (!isSameComponentOrHookScope(symbol, node)) return;
47852
47967
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47853
47968
  context.report({
@@ -48452,7 +48567,7 @@ const serverFetchWithoutRevalidate = defineRule({
48452
48567
  id: "server-fetch-without-revalidate",
48453
48568
  title: "Fetch without revalidate",
48454
48569
  severity: "warn",
48455
- disabledBy: ["nextjs:15"],
48570
+ disabledWhen: ["nextjs:15", "nextjs:static-export"],
48456
48571
  recommendation: "Pass `{ next: { revalidate: <seconds> } }` (or `cache: \"no-store\"`) so old data doesn't stick around.",
48457
48572
  create: (context) => {
48458
48573
  let isServerSideFile = false;
@@ -55419,6 +55534,34 @@ const reactDoctorRules = [
55419
55534
  ];
55420
55535
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55421
55536
  //#endregion
55537
+ //#region src/plugin/utils/is-next-file.ts
55538
+ const isNextFileActive = (context) => {
55539
+ const rawFilename = context.filename;
55540
+ if (!rawFilename) return true;
55541
+ const filename = normalizeFilename(rawFilename);
55542
+ const manifest = readNearestPackageManifest(filename);
55543
+ if (!manifest) return true;
55544
+ if (declaresDependency(manifest, "next")) return true;
55545
+ if (!declaresAnyDependency(manifest)) return true;
55546
+ const packageDirectory = findNearestPackageDirectory(filename);
55547
+ const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
55548
+ if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
55549
+ return true;
55550
+ };
55551
+ //#endregion
55552
+ //#region src/plugin/utils/wrap-nextjs-rule.ts
55553
+ const EMPTY_VISITORS$1 = {};
55554
+ const wrapNextjsRule = (rule) => {
55555
+ const innerCreate = rule.create.bind(rule);
55556
+ return {
55557
+ ...rule,
55558
+ create: (context) => {
55559
+ if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
55560
+ return innerCreate(context);
55561
+ }
55562
+ };
55563
+ };
55564
+ //#endregion
55422
55565
  //#region src/plugin/utils/wrap-react-native-rule.ts
55423
55566
  const EMPTY_VISITORS = {};
55424
55567
  const wrapReactNativeRule = (rule) => {
@@ -55888,9 +56031,14 @@ const wrapWithSemanticContext = (rule) => ({
55888
56031
  });
55889
56032
  //#endregion
55890
56033
  //#region src/plugin/react-doctor-plugin.ts
56034
+ const applyFrameworkGate = (rule) => {
56035
+ if (rule.framework === "react-native") return wrapReactNativeRule(rule);
56036
+ if (rule.framework === "nextjs") return wrapNextjsRule(rule);
56037
+ return rule;
56038
+ };
55891
56039
  const applyFrameworkRuleWrappers = (registry) => {
55892
56040
  const wrapped = {};
55893
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
56041
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55894
56042
  return wrapped;
55895
56043
  };
55896
56044
  const plugin = {
@@ -56227,9 +56375,23 @@ const classifySecurityScanFile = (relativePath) => {
56227
56375
  };
56228
56376
  const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGeneratedBundle || isProbablyTextFile(relativePath) || isConfigOrCiPath(relativePath) || isRepositorySecretFilePath(relativePath);
56229
56377
  //#endregion
56378
+ //#region src/plugin/utils/capability.ts
56379
+ const FRAMEWORK_TOKENS = [
56380
+ "nextjs",
56381
+ "vite",
56382
+ "cra",
56383
+ "remix",
56384
+ "gatsby",
56385
+ "expo",
56386
+ "react-native",
56387
+ "tanstack-start",
56388
+ "preact",
56389
+ "unknown"
56390
+ ];
56391
+ //#endregion
56230
56392
  //#region src/index.ts
56231
56393
  var src_default = plugin;
56232
56394
  //#endregion
56233
- export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
56395
+ export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, UNBOUNDED_CROSS_FILE_RULE_IDS, classifySecurityScanFile, collectCrossFileDependencyProbes, src_default as default, isReactNativeDependencyName, resetManifestCaches, shouldReadSecurityScanContent };
56234
56396
 
56235
56397
  //# sourceMappingURL=index.js.map