oxlint-plugin-react-doctor 0.6.0 → 0.6.1-dev.f07ee37

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 +217 -190
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -212,8 +212,16 @@ const sliceBelowSourceRoot = (filename) => {
212
212
  if (cutAt < 0) return filename;
213
213
  return filename.slice(cutAt);
214
214
  };
215
+ let lastFilename;
216
+ let lastResult = false;
215
217
  const isTestlikeFilename = (rawFilename) => {
216
218
  if (!rawFilename) return false;
219
+ if (rawFilename === lastFilename) return lastResult;
220
+ lastFilename = rawFilename;
221
+ lastResult = computeIsTestlikeFilename(rawFilename);
222
+ return lastResult;
223
+ };
224
+ const computeIsTestlikeFilename = (rawFilename) => {
217
225
  const filename = rawFilename.replaceAll("\\", "/");
218
226
  const lastSlash = filename.lastIndexOf("/");
219
227
  const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
@@ -983,16 +991,8 @@ const buildBindingIndex = (root) => {
983
991
  }
984
992
  }
985
993
  }
986
- const nodeRecord = node;
987
- for (const key of Object.keys(nodeRecord)) {
988
- if (key === "parent") continue;
989
- const child = nodeRecord[key];
990
- if (Array.isArray(child)) {
991
- for (const item of child) if (isAstNode(item)) visit(item);
992
- } else if (isAstNode(child)) visit(child);
993
- }
994
994
  };
995
- visit(root);
995
+ walkAst(root, visit);
996
996
  return out;
997
997
  };
998
998
  const programRootCache = /* @__PURE__ */ new WeakMap();
@@ -1423,49 +1423,40 @@ const getElementType = (openingElement, settings) => {
1423
1423
  //#region src/plugin/utils/find-import-source-for-name.ts
1424
1424
  const collectFromProgram = (programRoot) => {
1425
1425
  const lookup = /* @__PURE__ */ new Map();
1426
- const visit = (node) => {
1427
- if (node.type === "ImportDeclaration" && "source" in node && node.source) {
1428
- const source = node.source.value;
1429
- if (typeof source !== "string") return;
1430
- if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
1431
- if (!("local" in specifier) || !specifier.local) continue;
1432
- const local = specifier.local;
1433
- if (typeof local.name !== "string") continue;
1434
- if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
1435
- source,
1436
- imported: null,
1437
- isDefault: true,
1438
- isNamespace: false
1439
- });
1440
- else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
1426
+ const bodyStatements = programRoot.body ?? [];
1427
+ for (const node of bodyStatements) {
1428
+ if (node.type !== "ImportDeclaration" || !("source" in node) || !node.source) continue;
1429
+ const source = node.source.value;
1430
+ if (typeof source !== "string") continue;
1431
+ if (!("specifiers" in node) || !Array.isArray(node.specifiers)) continue;
1432
+ for (const specifier of node.specifiers) {
1433
+ if (!("local" in specifier) || !specifier.local) continue;
1434
+ const local = specifier.local;
1435
+ if (typeof local.name !== "string") continue;
1436
+ if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
1437
+ source,
1438
+ imported: null,
1439
+ isDefault: true,
1440
+ isNamespace: false
1441
+ });
1442
+ else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
1443
+ source,
1444
+ imported: null,
1445
+ isDefault: false,
1446
+ isNamespace: true
1447
+ });
1448
+ else if (specifier.type === "ImportSpecifier") {
1449
+ const importedNode = specifier.imported;
1450
+ const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
1451
+ lookup.set(local.name, {
1441
1452
  source,
1442
- imported: null,
1453
+ imported: importedName,
1443
1454
  isDefault: false,
1444
- isNamespace: true
1455
+ isNamespace: false
1445
1456
  });
1446
- else if (specifier.type === "ImportSpecifier") {
1447
- const importedNode = specifier.imported;
1448
- const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
1449
- lookup.set(local.name, {
1450
- source,
1451
- imported: importedName,
1452
- isDefault: false,
1453
- isNamespace: false
1454
- });
1455
- }
1456
1457
  }
1457
- return;
1458
- }
1459
- const nodeRecord = node;
1460
- for (const key of Object.keys(nodeRecord)) {
1461
- if (key === "parent") continue;
1462
- const child = nodeRecord[key];
1463
- if (Array.isArray(child)) {
1464
- for (const item of child) if (isAstNode(item)) visit(item);
1465
- } else if (isAstNode(child)) visit(child);
1466
1458
  }
1467
- };
1468
- visit(programRoot);
1459
+ }
1469
1460
  return lookup;
1470
1461
  };
1471
1462
  const importLookupCache = /* @__PURE__ */ new WeakMap();
@@ -1479,6 +1470,12 @@ const getImportLookup = (node) => {
1479
1470
  }
1480
1471
  return cached;
1481
1472
  };
1473
+ const hasImportFromModules = (contextNode, moduleSources) => {
1474
+ const lookup = getImportLookup(contextNode);
1475
+ if (!lookup) return false;
1476
+ for (const info of lookup.values()) if (moduleSources.includes(info.source)) return true;
1477
+ return false;
1478
+ };
1482
1479
  const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
1483
1480
  const lookup = getImportLookup(contextNode);
1484
1481
  if (!lookup) return false;
@@ -1573,7 +1570,6 @@ const LAYOUT_FILE_NAMES = [
1573
1570
  ];
1574
1571
  const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
1575
1572
  const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
1576
- const PAGES_DIRECTORY_PATTERN = /\/pages\//;
1577
1573
  const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
1578
1574
  "redirect",
1579
1575
  "permanentRedirect",
@@ -1591,7 +1587,6 @@ const CACHE_REVALIDATION_FUNCTION_NAMES = new Set([
1591
1587
  ]);
1592
1588
  const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
1593
1589
  const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
1594
- const APP_DIRECTORY_PATTERN = /\/app\//;
1595
1590
  const ROUTE_HANDLER_FILE_PATTERN = new RegExp(`/route\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1596
1591
  const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
1597
1592
  const MUTATING_ROUTE_SEGMENTS = new Set([
@@ -1628,15 +1623,16 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1628
1623
  };
1629
1624
  //#endregion
1630
1625
  //#region src/plugin/utils/normalize-filename.ts
1631
- const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
1626
+ const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1632
1627
  //#endregion
1633
1628
  //#region src/plugin/utils/is-generated-image-render-context.ts
1634
1629
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
1635
1630
  const SATORI_MODULE = "satori";
1631
+ const GENERATED_IMAGE_MODULES = [...IMAGE_RESPONSE_MODULES, SATORI_MODULE];
1636
1632
  const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
1637
1633
  const isGeneratedImageRenderFilename = (rawFilename) => {
1638
1634
  if (!rawFilename) return false;
1639
- return isNextjsMetadataImageRouteFilename(normalizeFilename$1(rawFilename));
1635
+ return isNextjsMetadataImageRouteFilename(normalizeFilename(rawFilename));
1640
1636
  };
1641
1637
  const isImageResponseCallee = (contextNode, callee) => {
1642
1638
  if (isNodeOfType(callee, "Identifier")) return IMAGE_RESPONSE_MODULES.some((moduleSource) => getImportedNameFromModule(contextNode, callee.name, moduleSource) === "ImageResponse");
@@ -1776,7 +1772,7 @@ const collectGeneratedImageJsxNodes = (programRoot) => {
1776
1772
  const cached = generatedImageJsxCache.get(programRoot);
1777
1773
  if (cached) return cached;
1778
1774
  const generatedImageJsxNodes = /* @__PURE__ */ new WeakSet();
1779
- walkAst(programRoot, (descendantNode) => {
1775
+ if (hasImportFromModules(programRoot, GENERATED_IMAGE_MODULES)) walkAst(programRoot, (descendantNode) => {
1780
1776
  if (!isGeneratedImageRendererCall(descendantNode)) return;
1781
1777
  for (const argument of descendantNode.arguments) markGeneratedImageExpression(argument, programRoot, generatedImageJsxNodes, /* @__PURE__ */ new Set());
1782
1778
  });
@@ -3969,9 +3965,11 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
3969
3965
  return;
3970
3966
  }
3971
3967
  if (typeof node.type === "string" && node.type.startsWith("TS")) return;
3972
- for (const [key, child] of Object.entries(node)) {
3968
+ const nodeRecord = node;
3969
+ for (const key of Object.keys(nodeRecord)) {
3973
3970
  if (key === "parent") continue;
3974
3971
  if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
3972
+ const child = nodeRecord[key];
3975
3973
  if (Array.isArray(child)) {
3976
3974
  for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
3977
3975
  } else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
@@ -4598,7 +4596,7 @@ const asyncParallel = defineRule({
4598
4596
  severity: "warn",
4599
4597
  recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",
4600
4598
  create: (context) => {
4601
- const filename = normalizeFilename$1(context.filename ?? "");
4599
+ const filename = normalizeFilename(context.filename ?? "");
4602
4600
  const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
4603
4601
  let hasTestLibraryImport = false;
4604
4602
  const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
@@ -8182,7 +8180,7 @@ const expoNoNonInlinedEnv = defineRule({
8182
8180
  severity: "warn",
8183
8181
  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.",
8184
8182
  create: (context) => {
8185
- const filename = normalizeFilename$1(context.filename ?? "");
8183
+ const filename = normalizeFilename(context.filename ?? "");
8186
8184
  if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$3;
8187
8185
  return {
8188
8186
  MemberExpression(node) {
@@ -10441,10 +10439,15 @@ const jsCombineIterations = defineRule({
10441
10439
  severity: "warn",
10442
10440
  recommendation: "Combine `.map().filter()` style chains into one pass with `.reduce()` or a `for...of` loop, so you only loop over the list once",
10443
10441
  create: (context) => {
10444
- let generatorNamesInFile = /* @__PURE__ */ new Set();
10442
+ let programNode = null;
10443
+ let generatorNamesInFile = null;
10444
+ const getGeneratorNamesInFile = () => {
10445
+ generatorNamesInFile ??= programNode ? collectGeneratorNames(programNode) : /* @__PURE__ */ new Set();
10446
+ return generatorNamesInFile;
10447
+ };
10445
10448
  return {
10446
- Program(programNode) {
10447
- generatorNamesInFile = collectGeneratorNames(programNode);
10449
+ Program(node) {
10450
+ programNode = node;
10448
10451
  },
10449
10452
  CallExpression(node) {
10450
10453
  if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
@@ -10466,7 +10469,7 @@ const jsCombineIterations = defineRule({
10466
10469
  if (isNullFilteringPredicate(filterArgument)) return;
10467
10470
  if (isTypePredicateArrow(filterArgument)) return;
10468
10471
  }
10469
- if (isReceiverChainIteratorRooted(innerCall.callee.object, generatorNamesInFile)) return;
10472
+ if (isReceiverChainIteratorRooted(innerCall.callee.object, getGeneratorNamesInFile())) return;
10470
10473
  if (isSmallLiteralArrayRootedChain(innerCall.callee.object)) return;
10471
10474
  if (isStringSplitRootedChain(innerCall.callee.object)) return;
10472
10475
  context.report({
@@ -11617,7 +11620,7 @@ const jsxFilenameExtension = defineRule({
11617
11620
  create: (context) => {
11618
11621
  const settings = resolveSettings$33(context.settings);
11619
11622
  const allowedExtensions = normalizeExtensions(settings.extensions);
11620
- const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
11623
+ const filename = normalizeFilename(context.filename ?? "fixture.tsx");
11621
11624
  const extensionOnly = path.extname(filename).slice(1);
11622
11625
  const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
11623
11626
  let didReportMismatch = false;
@@ -15257,6 +15260,52 @@ const nextjsAsyncClientComponent = defineRule({
15257
15260
  }
15258
15261
  });
15259
15262
  //#endregion
15263
+ //#region src/plugin/utils/get-project-relative-filename.ts
15264
+ const getProjectRelativeFilename = (filename, rootDirectory) => {
15265
+ const normalizedFilename = normalizeFilename(filename);
15266
+ if (!rootDirectory) return normalizedFilename;
15267
+ const rootDirectoryPrefix = `${normalizeFilename(rootDirectory).replace(/\/+$/, "")}/`;
15268
+ if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
15269
+ return normalizedFilename.slice(rootDirectoryPrefix.length);
15270
+ };
15271
+ //#endregion
15272
+ //#region src/plugin/utils/get-react-doctor-setting.ts
15273
+ const readReactDoctorSettingsBag = (settings) => {
15274
+ const reactDoctorSettings = settings?.["react-doctor"];
15275
+ if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
15276
+ return reactDoctorSettings;
15277
+ };
15278
+ const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
15279
+ const getReactDoctorStringSetting = (settings, settingName) => {
15280
+ const bag = readReactDoctorSettingsBag(settings);
15281
+ if (!bag) return void 0;
15282
+ const settingValue = readOwnPropertyValue(bag, settingName);
15283
+ return typeof settingValue === "string" ? settingValue : void 0;
15284
+ };
15285
+ const getReactDoctorNumberSetting = (settings, settingName) => {
15286
+ const bag = readReactDoctorSettingsBag(settings);
15287
+ if (!bag) return void 0;
15288
+ const settingValue = readOwnPropertyValue(bag, settingName);
15289
+ return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
15290
+ };
15291
+ const getReactDoctorStringArraySetting = (settings, settingName) => {
15292
+ const bag = readReactDoctorSettingsBag(settings);
15293
+ if (!bag) return [];
15294
+ const settingValue = readOwnPropertyValue(bag, settingName);
15295
+ if (!Array.isArray(settingValue)) return [];
15296
+ return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
15297
+ };
15298
+ //#endregion
15299
+ //#region src/plugin/utils/is-in-project-directory.ts
15300
+ const isInProjectDirectory = (context, directoryPath) => {
15301
+ const filename = normalizeFilename(context.filename ?? "");
15302
+ if (filename.length === 0) return false;
15303
+ const directorySegment = `/${directoryPath}/`;
15304
+ const relativeFilename = getProjectRelativeFilename(filename, getReactDoctorStringSetting(context.settings, "rootDirectory"));
15305
+ if (relativeFilename !== filename) return relativeFilename.startsWith(`${directoryPath}/`) || relativeFilename.includes(directorySegment);
15306
+ return filename.indexOf(directorySegment, 1) !== -1;
15307
+ };
15308
+ //#endregion
15260
15309
  //#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
15261
15310
  const nextjsErrorBoundaryMissingUseClient = defineRule({
15262
15311
  id: "nextjs-error-boundary-missing-use-client",
@@ -15266,8 +15315,8 @@ const nextjsErrorBoundaryMissingUseClient = defineRule({
15266
15315
  severity: "error",
15267
15316
  recommendation: "Add `'use client'` at the top of this file. Error boundaries must be Client Components to catch and render fallback UI",
15268
15317
  create: (context) => ({ Program(programNode) {
15269
- const filename = normalizeFilename$1(context.filename ?? "");
15270
- if (!APP_DIRECTORY_PATTERN.test(filename)) return;
15318
+ const filename = normalizeFilename(context.filename ?? "");
15319
+ if (!isInProjectDirectory(context, "app")) return;
15271
15320
  if (!ERROR_BOUNDARY_FILE_PATTERN.test(filename)) return;
15272
15321
  if (hasDirective(programNode, "use client")) return;
15273
15322
  context.report({
@@ -15298,8 +15347,8 @@ const nextjsGlobalErrorMissingHtmlBody = defineRule({
15298
15347
  severity: "error",
15299
15348
  recommendation: "Wrap your error UI in `<html><body>...</body></html>`. The root layout is unmounted when global-error renders",
15300
15349
  create: (context) => ({ Program(programNode) {
15301
- const filename = normalizeFilename$1(context.filename ?? "");
15302
- if (!APP_DIRECTORY_PATTERN.test(filename)) return;
15350
+ const filename = normalizeFilename(context.filename ?? "");
15351
+ if (!isInProjectDirectory(context, "app")) return;
15303
15352
  if (!GLOBAL_ERROR_FILE_PATTERN.test(filename)) return;
15304
15353
  const foundTags = fileContainsJsxElements(programNode, REQUIRED_HTML_TAGS);
15305
15354
  const missingTags = REQUIRED_HTML_TAGS.filter((tagName) => !foundTags.has(tagName)).map((tagName) => `<${tagName}>`);
@@ -15430,7 +15479,7 @@ const nextjsMissingMetadata = defineRule({
15430
15479
  severity: "warn",
15431
15480
  recommendation: "Add metadata or `generateMetadata()` so search engines and social previews get a title and description.",
15432
15481
  create: (context) => ({ Program(programNode) {
15433
- const filename = normalizeFilename$1(context.filename ?? "");
15482
+ const filename = normalizeFilename(context.filename ?? "");
15434
15483
  if (!PAGE_FILE_PATTERN.test(filename)) return;
15435
15484
  if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
15436
15485
  if (programNode.body?.some((statement) => {
@@ -15563,8 +15612,8 @@ const nextjsNoClientFetchForServerData = defineRule({
15563
15612
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15564
15613
  const callback = getEffectCallback(node);
15565
15614
  if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
15566
- const filename = normalizeFilename$1(context.filename ?? "");
15567
- if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
15615
+ const filename = normalizeFilename(context.filename ?? "");
15616
+ if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
15568
15617
  node,
15569
15618
  message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
15570
15619
  });
@@ -15663,8 +15712,8 @@ const nextjsNoDefaultExportInRouteHandler = defineRule({
15663
15712
  let programNode = null;
15664
15713
  return {
15665
15714
  Program(node) {
15666
- const filename = normalizeFilename$1(context.filename ?? "");
15667
- isAppRouteHandler = APP_DIRECTORY_PATTERN.test(filename) && ROUTE_HANDLER_FILE_PATTERN.test(filename);
15715
+ const filename = normalizeFilename(context.filename ?? "");
15716
+ isAppRouteHandler = isInProjectDirectory(context, "app") && ROUTE_HANDLER_FILE_PATTERN.test(filename);
15668
15717
  programNode = node;
15669
15718
  },
15670
15719
  ExportDefaultDeclaration(node) {
@@ -15700,7 +15749,7 @@ const nextjsNoEdgeOgRuntime = defineRule({
15700
15749
  let isOgImageFile = false;
15701
15750
  return {
15702
15751
  Program() {
15703
- const filename = normalizeFilename$1(context.filename ?? "");
15752
+ const filename = normalizeFilename(context.filename ?? "");
15704
15753
  isOgImageFile = OG_IMAGE_FILE_PATTERN.test(filename);
15705
15754
  },
15706
15755
  ExportNamedDeclaration(node) {
@@ -15776,8 +15825,7 @@ const nextjsNoHeadImport = defineRule({
15776
15825
  recommendation: "Use the Metadata API because `next/head` is ignored in the App Router and meta tags will not render.",
15777
15826
  create: (context) => ({ ImportDeclaration(node) {
15778
15827
  if (node.source?.value !== "next/head") return;
15779
- const filename = normalizeFilename$1(context.filename ?? "");
15780
- if (!APP_DIRECTORY_PATTERN.test(filename)) return;
15828
+ if (!isInProjectDirectory(context, "app")) return;
15781
15829
  context.report({
15782
15830
  node,
15783
15831
  message: "next/head silently does nothing in the App Router, so your meta tags never render."
@@ -16235,7 +16283,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16235
16283
  resolveBinding = buildProgramBindingLookup(node);
16236
16284
  },
16237
16285
  ExportNamedDeclaration(node) {
16238
- const filename = normalizeFilename$1(context.filename ?? "");
16286
+ const filename = normalizeFilename(context.filename ?? "");
16239
16287
  if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16240
16288
  if (CRON_ROUTE_PATTERN.test(filename)) return;
16241
16289
  if (!isExportedGetHandler(node)) return;
@@ -17014,7 +17062,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
17014
17062
  let suspenseLocalNames = /* @__PURE__ */ new Set();
17015
17063
  return {
17016
17064
  Program(programNode) {
17017
- const filename = normalizeFilename$1(context.filename ?? "");
17065
+ const filename = normalizeFilename(context.filename ?? "");
17018
17066
  isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
17019
17067
  if (!isPageOrLayoutFile) return;
17020
17068
  hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
@@ -17386,11 +17434,19 @@ const isSynchronous = (node, within) => {
17386
17434
  if (isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "void" || isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) return false;
17387
17435
  return isSynchronous(node.parent, within);
17388
17436
  };
17437
+ const unwrapUseCallback = (node) => {
17438
+ if (!node || !isNodeOfType(node, "CallExpression")) return node;
17439
+ const callee = node.callee;
17440
+ return isNodeOfType(callee, "Identifier") && callee.name === "useCallback" || isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useCallback" ? node.arguments?.[0] : node;
17441
+ };
17389
17442
  const resolveToFunction = (ref) => {
17390
17443
  const definitionNode = ref.resolved?.defs[0]?.node;
17391
17444
  if (!definitionNode) return null;
17392
17445
  if (isFunctionLike$1(definitionNode)) return definitionNode;
17393
- if (isNodeOfType(definitionNode, "VariableDeclarator") && isFunctionLike$1(definitionNode.init)) return definitionNode.init;
17446
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) {
17447
+ const initializer = unwrapUseCallback(definitionNode.init);
17448
+ if (isFunctionLike$1(initializer)) return initializer;
17449
+ }
17394
17450
  return null;
17395
17451
  };
17396
17452
  const resolvesToAsyncFunction = (ref) => Boolean(resolveToFunction(ref)?.async);
@@ -18658,40 +18714,13 @@ const classifyPackagePlatform = (filename) => {
18658
18714
  return result;
18659
18715
  };
18660
18716
  //#endregion
18661
- //#region src/plugin/utils/get-react-doctor-setting.ts
18662
- const readReactDoctorSettingsBag = (settings) => {
18663
- const reactDoctorSettings = settings?.["react-doctor"];
18664
- if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
18665
- return reactDoctorSettings;
18666
- };
18667
- const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
18668
- const getReactDoctorStringSetting = (settings, settingName) => {
18669
- const bag = readReactDoctorSettingsBag(settings);
18670
- if (!bag) return void 0;
18671
- const settingValue = readOwnPropertyValue(bag, settingName);
18672
- return typeof settingValue === "string" ? settingValue : void 0;
18673
- };
18674
- const getReactDoctorNumberSetting = (settings, settingName) => {
18675
- const bag = readReactDoctorSettingsBag(settings);
18676
- if (!bag) return void 0;
18677
- const settingValue = readOwnPropertyValue(bag, settingName);
18678
- return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
18679
- };
18680
- const getReactDoctorStringArraySetting = (settings, settingName) => {
18681
- const bag = readReactDoctorSettingsBag(settings);
18682
- if (!bag) return [];
18683
- const settingValue = readOwnPropertyValue(bag, settingName);
18684
- if (!Array.isArray(settingValue)) return [];
18685
- return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
18686
- };
18687
- //#endregion
18688
18717
  //#region src/plugin/utils/is-react-native-file.ts
18689
18718
  const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
18690
18719
  const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
18691
18720
  const classifyReactNativeFileTarget = (context) => {
18692
18721
  const rawFilename = context.filename;
18693
18722
  if (!rawFilename) return "unknown";
18694
- const filename = normalizeFilename$1(rawFilename);
18723
+ const filename = normalizeFilename(rawFilename);
18695
18724
  if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
18696
18725
  if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
18697
18726
  const packagePlatform = classifyPackagePlatform(filename);
@@ -18748,7 +18777,7 @@ const noBarrelImport = defineRule({
18748
18777
  if (didReportForFile) return;
18749
18778
  const source = node.source?.value;
18750
18779
  if (typeof source !== "string" || !source.startsWith(".")) return;
18751
- const filename = normalizeFilename$1(context.filename ?? "");
18780
+ const filename = normalizeFilename(context.filename ?? "");
18752
18781
  if (!filename) return;
18753
18782
  const importRequests = getRuntimeImportRequests(node);
18754
18783
  if (importRequests.length === 0) return;
@@ -26219,15 +26248,6 @@ const CLIENT_APP_DIRECTORY_FRAMEWORKS = new Set([
26219
26248
  "vite"
26220
26249
  ]);
26221
26250
  const isInsideDirectory = (pathSegments, directoryNames) => pathSegments.some((pathSegment) => directoryNames.has(pathSegment));
26222
- const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
26223
- const normalizeDirectory = (directory) => normalizeFilename(directory).replace(/\/+$/, "");
26224
- const getProjectRelativeFilename = (filename, rootDirectory) => {
26225
- const normalizedFilename = normalizeFilename(filename);
26226
- if (!rootDirectory) return normalizedFilename;
26227
- const rootDirectoryPrefix = `${normalizeDirectory(rootDirectory)}/`;
26228
- if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
26229
- return normalizedFilename.slice(rootDirectoryPrefix.length);
26230
- };
26231
26251
  const getClassifiablePathSegments = (pathSegments) => {
26232
26252
  const srcIndex = pathSegments.lastIndexOf("src");
26233
26253
  if (srcIndex === -1) return pathSegments;
@@ -26289,7 +26309,6 @@ const tokenizeIdentifierWords = (identifierName) => {
26289
26309
  const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
26290
26310
  //#endregion
26291
26311
  //#region src/plugin/constants/tanstack.ts
26292
- const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
26293
26312
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
26294
26313
  const TANSTACK_ROUTE_PROPERTY_ORDER = [
26295
26314
  "params",
@@ -26401,7 +26420,7 @@ const noSecretsInClientCode = defineRule({
26401
26420
  severity: "warn",
26402
26421
  recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
26403
26422
  create: (context) => {
26404
- const filename = normalizeFilename$1(context.filename ?? "");
26423
+ const filename = normalizeFilename(context.filename ?? "");
26405
26424
  const framework = getReactDoctorStringSetting(context.settings, "framework");
26406
26425
  const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
26407
26426
  let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
@@ -29507,21 +29526,18 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
29507
29526
  reportNode
29508
29527
  };
29509
29528
  };
29510
- const collectAllNodes = (programRoot) => {
29511
- const out = [];
29512
- const visit = (node) => {
29513
- out.push(node);
29514
- const record = node;
29515
- for (const key of Object.keys(record)) {
29516
- if (key === "parent") continue;
29517
- const child = record[key];
29518
- if (Array.isArray(child)) {
29519
- for (const item of child) if (isAstNode(item)) visit(item);
29520
- } else if (isAstNode(child)) visit(child);
29521
- }
29529
+ const collectRelevantNodes = (programRoot) => {
29530
+ const exportNodes = [];
29531
+ const componentCandidates = [];
29532
+ walkAst(programRoot, (child) => {
29533
+ const childType = child.type;
29534
+ if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
29535
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
29536
+ });
29537
+ return {
29538
+ exportNodes,
29539
+ componentCandidates
29522
29540
  };
29523
- visit(programRoot);
29524
- return out;
29525
29541
  };
29526
29542
  const isEntryPointFile = (filename) => {
29527
29543
  const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
@@ -29567,14 +29583,14 @@ const onlyExportComponents = defineRule({
29567
29583
  allowConstantExport: settings.allowConstantExport
29568
29584
  };
29569
29585
  return { Program(node) {
29570
- if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
29571
- const allNodes = collectAllNodes(node);
29586
+ if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
29587
+ const { exportNodes, componentCandidates } = collectRelevantNodes(node);
29572
29588
  const exports = [];
29573
29589
  let hasReactExport = false;
29574
29590
  let hasAnyExports = false;
29575
29591
  const localComponents = [];
29576
29592
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
29577
- for (const child of allNodes) {
29593
+ for (const child of exportNodes) {
29578
29594
  if (isNodeOfType(child, "ExportAllDeclaration")) {
29579
29595
  if (child.exportKind === "type") continue;
29580
29596
  hasAnyExports = true;
@@ -29710,7 +29726,7 @@ const onlyExportComponents = defineRule({
29710
29726
  }
29711
29727
  return false;
29712
29728
  };
29713
- for (const child of allNodes) {
29729
+ for (const child of componentCandidates) {
29714
29730
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
29715
29731
  if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
29716
29732
  }
@@ -32001,6 +32017,7 @@ const renderingHydrationMismatchTime = defineRule({
32001
32017
  });
32002
32018
  //#endregion
32003
32019
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
32020
+ const USE_EFFECT_ONLY = new Set(["useEffect"]);
32004
32021
  const renderingHydrationNoFlicker = defineRule({
32005
32022
  id: "rendering-hydration-no-flicker",
32006
32023
  title: "useEffect setState flashes on mount",
@@ -32008,7 +32025,7 @@ const renderingHydrationNoFlicker = defineRule({
32008
32025
  severity: "warn",
32009
32026
  recommendation: "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
32010
32027
  create: (context) => ({ CallExpression(node) {
32011
- if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
32028
+ if (!isHookCall$1(node, USE_EFFECT_ONLY) || (node.arguments?.length ?? 0) < 2) return;
32012
32029
  const depsNode = node.arguments[1];
32013
32030
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
32014
32031
  const callback = getEffectCallback(node);
@@ -32081,7 +32098,7 @@ const renderingSvgPrecision = defineRule({
32081
32098
  recommendation: "Round path, points, and transform decimals to 1 or 2 digits. The extra precision adds bytes with no visible difference.",
32082
32099
  create: (context) => {
32083
32100
  const filename = context.filename;
32084
- const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
32101
+ const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename(filename) : void 0);
32085
32102
  return { JSXAttribute(node) {
32086
32103
  if (isAutoGenerated) return;
32087
32104
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -33231,7 +33248,7 @@ const rnDetoxMissingAwait = defineRule({
33231
33248
  severity: "warn",
33232
33249
  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.",
33233
33250
  create: (context) => {
33234
- const filename = normalizeFilename$1(context.filename ?? "");
33251
+ const filename = normalizeFilename(context.filename ?? "");
33235
33252
  if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$2;
33236
33253
  return { ExpressionStatement(node) {
33237
33254
  const expression = node.expression;
@@ -34961,7 +34978,7 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
34961
34978
  //#region src/plugin/utils/is-expo-managed-file.ts
34962
34979
  const isExpoManagedFileActive = (context) => {
34963
34980
  const rawFilename = context.filename;
34964
- const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
34981
+ const filename = rawFilename ? normalizeFilename(rawFilename) : void 0;
34965
34982
  if (filename) {
34966
34983
  const packagePlatform = classifyPackagePlatform(filename);
34967
34984
  if (packagePlatform === "expo") return true;
@@ -35735,6 +35752,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
35735
35752
  "aria-labelledby",
35736
35753
  "aria-live",
35737
35754
  "aria-owns",
35755
+ "aria-readonly",
35738
35756
  "aria-relevant",
35739
35757
  "aria-required",
35740
35758
  "aria-roledescription"
@@ -35785,6 +35803,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
35785
35803
  "aria-labelledby",
35786
35804
  "aria-live",
35787
35805
  "aria-owns",
35806
+ "aria-readonly",
35788
35807
  "aria-relevant",
35789
35808
  "aria-required",
35790
35809
  "aria-roledescription",
@@ -35816,6 +35835,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
35816
35835
  "aria-labelledby",
35817
35836
  "aria-live",
35818
35837
  "aria-owns",
35838
+ "aria-readonly",
35819
35839
  "aria-relevant",
35820
35840
  "aria-required",
35821
35841
  "aria-roledescription"
@@ -37114,7 +37134,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37114
37134
  "aria-label",
37115
37135
  "aria-labelledby",
37116
37136
  "aria-live",
37137
+ "aria-multiselectable",
37117
37138
  "aria-owns",
37139
+ "aria-readonly",
37118
37140
  "aria-relevant",
37119
37141
  "aria-roledescription",
37120
37142
  "aria-rowcount"
@@ -37142,6 +37164,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37142
37164
  "aria-labelledby",
37143
37165
  "aria-live",
37144
37166
  "aria-owns",
37167
+ "aria-readonly",
37145
37168
  "aria-relevant",
37146
37169
  "aria-required",
37147
37170
  "aria-roledescription",
@@ -37331,8 +37354,10 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37331
37354
  "aria-label",
37332
37355
  "aria-labelledby",
37333
37356
  "aria-live",
37357
+ "aria-multiselectable",
37334
37358
  "aria-orientation",
37335
37359
  "aria-owns",
37360
+ "aria-readonly",
37336
37361
  "aria-relevant",
37337
37362
  "aria-required",
37338
37363
  "aria-roledescription"
@@ -37548,6 +37573,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37548
37573
  "aria-live",
37549
37574
  "aria-owns",
37550
37575
  "aria-posinset",
37576
+ "aria-readonly",
37551
37577
  "aria-relevant",
37552
37578
  "aria-required",
37553
37579
  "aria-roledescription",
@@ -37576,6 +37602,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37576
37602
  "aria-live",
37577
37603
  "aria-owns",
37578
37604
  "aria-posinset",
37605
+ "aria-readonly",
37579
37606
  "aria-relevant",
37580
37607
  "aria-required",
37581
37608
  "aria-roledescription",
@@ -37778,6 +37805,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37778
37805
  "aria-live",
37779
37806
  "aria-orientation",
37780
37807
  "aria-owns",
37808
+ "aria-readonly",
37781
37809
  "aria-relevant",
37782
37810
  "aria-required",
37783
37811
  "aria-roledescription"
@@ -37912,6 +37940,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37912
37940
  "aria-labelledby",
37913
37941
  "aria-live",
37914
37942
  "aria-owns",
37943
+ "aria-readonly",
37915
37944
  "aria-relevant",
37916
37945
  "aria-required",
37917
37946
  "aria-roledescription",
@@ -37988,6 +38017,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37988
38017
  "aria-multiline",
37989
38018
  "aria-owns",
37990
38019
  "aria-placeholder",
38020
+ "aria-readonly",
37991
38021
  "aria-relevant",
37992
38022
  "aria-required",
37993
38023
  "aria-roledescription"
@@ -38098,10 +38128,11 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38098
38128
  "aria-live",
38099
38129
  "aria-orientation",
38100
38130
  "aria-owns",
38131
+ "aria-readonly",
38101
38132
  "aria-relevant",
38102
38133
  "aria-roledescription",
38103
- "aria-valuemin",
38104
38134
  "aria-valuemax",
38135
+ "aria-valuemin",
38105
38136
  "aria-valuenow",
38106
38137
  "aria-valuetext"
38107
38138
  ]),
@@ -38125,6 +38156,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38125
38156
  "aria-labelledby",
38126
38157
  "aria-live",
38127
38158
  "aria-owns",
38159
+ "aria-readonly",
38128
38160
  "aria-relevant",
38129
38161
  "aria-required",
38130
38162
  "aria-roledescription",
@@ -38258,6 +38290,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38258
38290
  "aria-labelledby",
38259
38291
  "aria-live",
38260
38292
  "aria-owns",
38293
+ "aria-readonly",
38261
38294
  "aria-relevant",
38262
38295
  "aria-required",
38263
38296
  "aria-roledescription"
@@ -38326,6 +38359,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38326
38359
  "aria-labelledby",
38327
38360
  "aria-level",
38328
38361
  "aria-live",
38362
+ "aria-multiselectable",
38329
38363
  "aria-orientation",
38330
38364
  "aria-owns",
38331
38365
  "aria-relevant",
@@ -38393,6 +38427,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38393
38427
  "aria-multiline",
38394
38428
  "aria-owns",
38395
38429
  "aria-placeholder",
38430
+ "aria-readonly",
38396
38431
  "aria-relevant",
38397
38432
  "aria-required",
38398
38433
  "aria-roledescription"
@@ -38495,6 +38530,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38495
38530
  "aria-label",
38496
38531
  "aria-labelledby",
38497
38532
  "aria-live",
38533
+ "aria-multiselectable",
38498
38534
  "aria-orientation",
38499
38535
  "aria-owns",
38500
38536
  "aria-relevant",
@@ -38512,6 +38548,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38512
38548
  "aria-details",
38513
38549
  "aria-disabled",
38514
38550
  "aria-dropeffect",
38551
+ "aria-errormessage",
38515
38552
  "aria-flowto",
38516
38553
  "aria-grabbed",
38517
38554
  "aria-hidden",
@@ -38520,8 +38557,10 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38520
38557
  "aria-label",
38521
38558
  "aria-labelledby",
38522
38559
  "aria-live",
38560
+ "aria-multiselectable",
38523
38561
  "aria-orientation",
38524
38562
  "aria-owns",
38563
+ "aria-readonly",
38525
38564
  "aria-relevant",
38526
38565
  "aria-required",
38527
38566
  "aria-roledescription",
@@ -39553,8 +39592,8 @@ const serverFetchWithoutRevalidate = defineRule({
39553
39592
  let isServerSideFile = false;
39554
39593
  return {
39555
39594
  Program(node) {
39556
- const filename = normalizeFilename$1(context.filename ?? "");
39557
- if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
39595
+ const filename = normalizeFilename(context.filename ?? "");
39596
+ if (!isInProjectDirectory(context, "app") || !APP_ROUTER_FILE_PATTERN.test(filename)) {
39558
39597
  isServerSideFile = false;
39559
39598
  return;
39560
39599
  }
@@ -39639,7 +39678,6 @@ const collectRequestTaintedNames = (handlerBody, handlerParamNames) => {
39639
39678
  });
39640
39679
  return taintedNames;
39641
39680
  };
39642
- const PAGES_ROUTER_API_PATH_PATTERN = /\/pages\/api\//;
39643
39681
  const inspectHandlerBody = (context, handlerBody, handlerLabel, handlerParamNames) => {
39644
39682
  const requestTaintedNames = collectRequestTaintedNames(handlerBody, handlerParamNames);
39645
39683
  walkAst(handlerBody, (child) => {
@@ -39680,8 +39718,7 @@ const serverHoistStaticIo = defineRule({
39680
39718
  inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectHandlerParamNames(declaration.params ?? []));
39681
39719
  },
39682
39720
  ExportDefaultDeclaration(node) {
39683
- const filename = normalizeFilename$1(context.filename ?? "");
39684
- if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
39721
+ if (!isInProjectDirectory(context, "pages/api")) return;
39685
39722
  const declaration = node.declaration;
39686
39723
  if (!isFunctionLike$1(declaration)) return;
39687
39724
  if (!declaration.async) return;
@@ -40735,7 +40772,7 @@ const tanstackStartMissingHeadContent = defineRule({
40735
40772
  collectVariableAlias(node);
40736
40773
  },
40737
40774
  JSXOpeningElement(node) {
40738
- const filename = normalizeFilename$1(context.filename ?? "");
40775
+ const filename = normalizeFilename(context.filename ?? "");
40739
40776
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40740
40777
  if (isNodeOfType(node.name, "JSXIdentifier")) {
40741
40778
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
@@ -40750,7 +40787,7 @@ const tanstackStartMissingHeadContent = defineRule({
40750
40787
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
40751
40788
  },
40752
40789
  "Program:exit"(programNode) {
40753
- const filename = normalizeFilename$1(context.filename ?? "");
40790
+ const filename = normalizeFilename(context.filename ?? "");
40754
40791
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40755
40792
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
40756
40793
  node: programNode,
@@ -40776,8 +40813,7 @@ const tanstackStartNoAnchorElement = defineRule({
40776
40813
  severity: "warn",
40777
40814
  recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
40778
40815
  create: (context) => ({ JSXOpeningElement(node) {
40779
- const filename = normalizeFilename$1(context.filename ?? "");
40780
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40816
+ if (!isInProjectDirectory(context, "routes")) return;
40781
40817
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40782
40818
  const attributes = node.attributes ?? [];
40783
40819
  const hrefAttribute = findJsxAttribute(attributes, "href");
@@ -40860,6 +40896,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40860
40896
  severity: "warn",
40861
40897
  recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
40862
40898
  create: (context) => {
40899
+ const isRouteFile = isInProjectDirectory(context, "routes");
40863
40900
  let deferredCallbackDepth = 0;
40864
40901
  let eventHandlerDepth = 0;
40865
40902
  const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
@@ -40923,8 +40960,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40923
40960
  };
40924
40961
  return {
40925
40962
  CallExpression(node) {
40926
- const filename = normalizeFilename$1(context.filename ?? "");
40927
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40963
+ if (!isRouteFile) return;
40928
40964
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
40929
40965
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
40930
40966
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
@@ -40936,48 +40972,39 @@ const tanstackStartNoNavigateInRender = defineRule({
40936
40972
  }
40937
40973
  },
40938
40974
  "CallExpression:exit"(node) {
40939
- const filename = normalizeFilename$1(context.filename ?? "");
40940
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40975
+ if (!isRouteFile) return;
40941
40976
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
40942
40977
  },
40943
40978
  JSXAttribute(node) {
40944
- const filename = normalizeFilename$1(context.filename ?? "");
40945
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40979
+ if (!isRouteFile) return;
40946
40980
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
40947
40981
  },
40948
40982
  "JSXAttribute:exit"(node) {
40949
- const filename = normalizeFilename$1(context.filename ?? "");
40950
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40983
+ if (!isRouteFile) return;
40951
40984
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40952
40985
  },
40953
40986
  Property(node) {
40954
- const filename = normalizeFilename$1(context.filename ?? "");
40955
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40987
+ if (!isRouteFile) return;
40956
40988
  if (isEventHandlerProperty(node)) eventHandlerDepth++;
40957
40989
  },
40958
40990
  "Property:exit"(node) {
40959
- const filename = normalizeFilename$1(context.filename ?? "");
40960
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40991
+ if (!isRouteFile) return;
40961
40992
  if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40962
40993
  },
40963
40994
  VariableDeclarator(node) {
40964
- const filename = normalizeFilename$1(context.filename ?? "");
40965
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40995
+ if (!isRouteFile) return;
40966
40996
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
40967
40997
  },
40968
40998
  "VariableDeclarator:exit"(node) {
40969
- const filename = normalizeFilename$1(context.filename ?? "");
40970
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40999
+ if (!isRouteFile) return;
40971
41000
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40972
41001
  },
40973
41002
  FunctionDeclaration(node) {
40974
- const filename = normalizeFilename$1(context.filename ?? "");
40975
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
41003
+ if (!isRouteFile) return;
40976
41004
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
40977
41005
  },
40978
41006
  "FunctionDeclaration:exit"(node) {
40979
- const filename = normalizeFilename$1(context.filename ?? "");
40980
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
41007
+ if (!isRouteFile) return;
40981
41008
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40982
41009
  }
40983
41010
  };
@@ -41062,8 +41089,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
41062
41089
  severity: "warn",
41063
41090
  recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
41064
41091
  create: (context) => ({ CallExpression(node) {
41065
- const filename = normalizeFilename$1(context.filename ?? "");
41066
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
41092
+ if (!isInProjectDirectory(context, "routes")) return;
41067
41093
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41068
41094
  const callback = node.arguments?.[0];
41069
41095
  if (!callback) return;
@@ -46817,23 +46843,29 @@ const FALLBACK_CFG = {
46817
46843
  enclosingFunction: () => null,
46818
46844
  isUnconditionalFromEntry: () => false
46819
46845
  };
46846
+ const scopesByProgram = /* @__PURE__ */ new WeakMap();
46847
+ const cfgByProgram = /* @__PURE__ */ new WeakMap();
46820
46848
  const wrapWithSemanticContext = (rule) => ({
46821
46849
  ...rule,
46822
46850
  create: (baseContext) => {
46823
46851
  let programRoot = null;
46824
- let cachedScopes = null;
46825
- let cachedCfg = null;
46826
46852
  const getScopes = () => {
46827
- if (cachedScopes) return cachedScopes;
46828
46853
  if (!programRoot) return buildFallbackScopes();
46829
- cachedScopes = analyzeScopes(programRoot);
46830
- return cachedScopes;
46854
+ let scopes = scopesByProgram.get(programRoot);
46855
+ if (!scopes) {
46856
+ scopes = analyzeScopes(programRoot);
46857
+ scopesByProgram.set(programRoot, scopes);
46858
+ }
46859
+ return scopes;
46831
46860
  };
46832
46861
  const getCfg = () => {
46833
- if (cachedCfg) return cachedCfg;
46834
46862
  if (!programRoot) return FALLBACK_CFG;
46835
- cachedCfg = analyzeControlFlow(programRoot);
46836
- return cachedCfg;
46863
+ let cfg = cfgByProgram.get(programRoot);
46864
+ if (!cfg) {
46865
+ cfg = analyzeControlFlow(programRoot);
46866
+ cfgByProgram.set(programRoot, cfg);
46867
+ }
46868
+ return cfg;
46837
46869
  };
46838
46870
  const enrichedContext = {
46839
46871
  report: baseContext.report,
@@ -46848,23 +46880,18 @@ const wrapWithSemanticContext = (rule) => ({
46848
46880
  return getCfg();
46849
46881
  }
46850
46882
  };
46851
- const captureRootIfNeeded = (node) => {
46852
- if (programRoot) return;
46853
- programRoot = findProgramRoot(node);
46854
- };
46855
46883
  const visitors = rule.create(enrichedContext);
46856
- const wrappedVisitors = {};
46884
+ const passthroughVisitors = {};
46857
46885
  for (const [nodeType, handler] of Object.entries(visitors)) {
46858
46886
  if (typeof handler !== "function") continue;
46859
- wrappedVisitors[nodeType] = ((node) => {
46860
- captureRootIfNeeded(node);
46861
- handler(node);
46862
- });
46887
+ passthroughVisitors[nodeType] = handler;
46863
46888
  }
46864
- if (!visitors.Program) wrappedVisitors.Program = (node) => {
46865
- captureRootIfNeeded(node);
46866
- };
46867
- return wrappedVisitors;
46889
+ const innerProgramHandler = passthroughVisitors.Program;
46890
+ passthroughVisitors.Program = ((node) => {
46891
+ programRoot = node;
46892
+ if (innerProgramHandler) innerProgramHandler(node);
46893
+ });
46894
+ return passthroughVisitors;
46868
46895
  }
46869
46896
  });
46870
46897
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-dev.f07ee37",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",