oxlint-plugin-react-doctor 0.6.0-dev.113637e → 0.6.0-dev.5f60bef

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 +123 -99
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1573,7 +1573,6 @@ const LAYOUT_FILE_NAMES = [
1573
1573
  ];
1574
1574
  const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
1575
1575
  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
1576
  const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
1578
1577
  "redirect",
1579
1578
  "permanentRedirect",
@@ -1591,7 +1590,6 @@ const CACHE_REVALIDATION_FUNCTION_NAMES = new Set([
1591
1590
  ]);
1592
1591
  const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
1593
1592
  const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
1594
- const APP_DIRECTORY_PATTERN = /\/app\//;
1595
1593
  const ROUTE_HANDLER_FILE_PATTERN = new RegExp(`/route\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1596
1594
  const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
1597
1595
  const MUTATING_ROUTE_SEGMENTS = new Set([
@@ -1628,7 +1626,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1628
1626
  };
1629
1627
  //#endregion
1630
1628
  //#region src/plugin/utils/normalize-filename.ts
1631
- const normalizeFilename$1 = (filename) => filename.replaceAll("\\", "/");
1629
+ const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1632
1630
  //#endregion
1633
1631
  //#region src/plugin/utils/is-generated-image-render-context.ts
1634
1632
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -1636,7 +1634,7 @@ const SATORI_MODULE = "satori";
1636
1634
  const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
1637
1635
  const isGeneratedImageRenderFilename = (rawFilename) => {
1638
1636
  if (!rawFilename) return false;
1639
- return isNextjsMetadataImageRouteFilename(normalizeFilename$1(rawFilename));
1637
+ return isNextjsMetadataImageRouteFilename(normalizeFilename(rawFilename));
1640
1638
  };
1641
1639
  const isImageResponseCallee = (contextNode, callee) => {
1642
1640
  if (isNodeOfType(callee, "Identifier")) return IMAGE_RESPONSE_MODULES.some((moduleSource) => getImportedNameFromModule(contextNode, callee.name, moduleSource) === "ImageResponse");
@@ -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) {
@@ -11617,7 +11615,7 @@ const jsxFilenameExtension = defineRule({
11617
11615
  create: (context) => {
11618
11616
  const settings = resolveSettings$33(context.settings);
11619
11617
  const allowedExtensions = normalizeExtensions(settings.extensions);
11620
- const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
11618
+ const filename = normalizeFilename(context.filename ?? "fixture.tsx");
11621
11619
  const extensionOnly = path.extname(filename).slice(1);
11622
11620
  const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
11623
11621
  let didReportMismatch = false;
@@ -15257,6 +15255,52 @@ const nextjsAsyncClientComponent = defineRule({
15257
15255
  }
15258
15256
  });
15259
15257
  //#endregion
15258
+ //#region src/plugin/utils/get-project-relative-filename.ts
15259
+ const getProjectRelativeFilename = (filename, rootDirectory) => {
15260
+ const normalizedFilename = normalizeFilename(filename);
15261
+ if (!rootDirectory) return normalizedFilename;
15262
+ const rootDirectoryPrefix = `${normalizeFilename(rootDirectory).replace(/\/+$/, "")}/`;
15263
+ if (!normalizedFilename.startsWith(rootDirectoryPrefix)) return normalizedFilename;
15264
+ return normalizedFilename.slice(rootDirectoryPrefix.length);
15265
+ };
15266
+ //#endregion
15267
+ //#region src/plugin/utils/get-react-doctor-setting.ts
15268
+ const readReactDoctorSettingsBag = (settings) => {
15269
+ const reactDoctorSettings = settings?.["react-doctor"];
15270
+ if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
15271
+ return reactDoctorSettings;
15272
+ };
15273
+ const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
15274
+ const getReactDoctorStringSetting = (settings, settingName) => {
15275
+ const bag = readReactDoctorSettingsBag(settings);
15276
+ if (!bag) return void 0;
15277
+ const settingValue = readOwnPropertyValue(bag, settingName);
15278
+ return typeof settingValue === "string" ? settingValue : void 0;
15279
+ };
15280
+ const getReactDoctorNumberSetting = (settings, settingName) => {
15281
+ const bag = readReactDoctorSettingsBag(settings);
15282
+ if (!bag) return void 0;
15283
+ const settingValue = readOwnPropertyValue(bag, settingName);
15284
+ return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
15285
+ };
15286
+ const getReactDoctorStringArraySetting = (settings, settingName) => {
15287
+ const bag = readReactDoctorSettingsBag(settings);
15288
+ if (!bag) return [];
15289
+ const settingValue = readOwnPropertyValue(bag, settingName);
15290
+ if (!Array.isArray(settingValue)) return [];
15291
+ return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
15292
+ };
15293
+ //#endregion
15294
+ //#region src/plugin/utils/is-in-project-directory.ts
15295
+ const isInProjectDirectory = (context, directoryPath) => {
15296
+ const filename = normalizeFilename(context.filename ?? "");
15297
+ if (filename.length === 0) return false;
15298
+ const directorySegment = `/${directoryPath}/`;
15299
+ const relativeFilename = getProjectRelativeFilename(filename, getReactDoctorStringSetting(context.settings, "rootDirectory"));
15300
+ if (relativeFilename !== filename) return relativeFilename.startsWith(`${directoryPath}/`) || relativeFilename.includes(directorySegment);
15301
+ return filename.indexOf(directorySegment, 1) !== -1;
15302
+ };
15303
+ //#endregion
15260
15304
  //#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
15261
15305
  const nextjsErrorBoundaryMissingUseClient = defineRule({
15262
15306
  id: "nextjs-error-boundary-missing-use-client",
@@ -15266,8 +15310,8 @@ const nextjsErrorBoundaryMissingUseClient = defineRule({
15266
15310
  severity: "error",
15267
15311
  recommendation: "Add `'use client'` at the top of this file. Error boundaries must be Client Components to catch and render fallback UI",
15268
15312
  create: (context) => ({ Program(programNode) {
15269
- const filename = normalizeFilename$1(context.filename ?? "");
15270
- if (!APP_DIRECTORY_PATTERN.test(filename)) return;
15313
+ const filename = normalizeFilename(context.filename ?? "");
15314
+ if (!isInProjectDirectory(context, "app")) return;
15271
15315
  if (!ERROR_BOUNDARY_FILE_PATTERN.test(filename)) return;
15272
15316
  if (hasDirective(programNode, "use client")) return;
15273
15317
  context.report({
@@ -15298,8 +15342,8 @@ const nextjsGlobalErrorMissingHtmlBody = defineRule({
15298
15342
  severity: "error",
15299
15343
  recommendation: "Wrap your error UI in `<html><body>...</body></html>`. The root layout is unmounted when global-error renders",
15300
15344
  create: (context) => ({ Program(programNode) {
15301
- const filename = normalizeFilename$1(context.filename ?? "");
15302
- if (!APP_DIRECTORY_PATTERN.test(filename)) return;
15345
+ const filename = normalizeFilename(context.filename ?? "");
15346
+ if (!isInProjectDirectory(context, "app")) return;
15303
15347
  if (!GLOBAL_ERROR_FILE_PATTERN.test(filename)) return;
15304
15348
  const foundTags = fileContainsJsxElements(programNode, REQUIRED_HTML_TAGS);
15305
15349
  const missingTags = REQUIRED_HTML_TAGS.filter((tagName) => !foundTags.has(tagName)).map((tagName) => `<${tagName}>`);
@@ -15430,7 +15474,7 @@ const nextjsMissingMetadata = defineRule({
15430
15474
  severity: "warn",
15431
15475
  recommendation: "Add metadata or `generateMetadata()` so search engines and social previews get a title and description.",
15432
15476
  create: (context) => ({ Program(programNode) {
15433
- const filename = normalizeFilename$1(context.filename ?? "");
15477
+ const filename = normalizeFilename(context.filename ?? "");
15434
15478
  if (!PAGE_FILE_PATTERN.test(filename)) return;
15435
15479
  if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
15436
15480
  if (programNode.body?.some((statement) => {
@@ -15563,8 +15607,8 @@ const nextjsNoClientFetchForServerData = defineRule({
15563
15607
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15564
15608
  const callback = getEffectCallback(node);
15565
15609
  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({
15610
+ const filename = normalizeFilename(context.filename ?? "");
15611
+ if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
15568
15612
  node,
15569
15613
  message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
15570
15614
  });
@@ -15663,8 +15707,8 @@ const nextjsNoDefaultExportInRouteHandler = defineRule({
15663
15707
  let programNode = null;
15664
15708
  return {
15665
15709
  Program(node) {
15666
- const filename = normalizeFilename$1(context.filename ?? "");
15667
- isAppRouteHandler = APP_DIRECTORY_PATTERN.test(filename) && ROUTE_HANDLER_FILE_PATTERN.test(filename);
15710
+ const filename = normalizeFilename(context.filename ?? "");
15711
+ isAppRouteHandler = isInProjectDirectory(context, "app") && ROUTE_HANDLER_FILE_PATTERN.test(filename);
15668
15712
  programNode = node;
15669
15713
  },
15670
15714
  ExportDefaultDeclaration(node) {
@@ -15700,7 +15744,7 @@ const nextjsNoEdgeOgRuntime = defineRule({
15700
15744
  let isOgImageFile = false;
15701
15745
  return {
15702
15746
  Program() {
15703
- const filename = normalizeFilename$1(context.filename ?? "");
15747
+ const filename = normalizeFilename(context.filename ?? "");
15704
15748
  isOgImageFile = OG_IMAGE_FILE_PATTERN.test(filename);
15705
15749
  },
15706
15750
  ExportNamedDeclaration(node) {
@@ -15776,8 +15820,7 @@ const nextjsNoHeadImport = defineRule({
15776
15820
  recommendation: "Use the Metadata API because `next/head` is ignored in the App Router and meta tags will not render.",
15777
15821
  create: (context) => ({ ImportDeclaration(node) {
15778
15822
  if (node.source?.value !== "next/head") return;
15779
- const filename = normalizeFilename$1(context.filename ?? "");
15780
- if (!APP_DIRECTORY_PATTERN.test(filename)) return;
15823
+ if (!isInProjectDirectory(context, "app")) return;
15781
15824
  context.report({
15782
15825
  node,
15783
15826
  message: "next/head silently does nothing in the App Router, so your meta tags never render."
@@ -16235,7 +16278,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16235
16278
  resolveBinding = buildProgramBindingLookup(node);
16236
16279
  },
16237
16280
  ExportNamedDeclaration(node) {
16238
- const filename = normalizeFilename$1(context.filename ?? "");
16281
+ const filename = normalizeFilename(context.filename ?? "");
16239
16282
  if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16240
16283
  if (CRON_ROUTE_PATTERN.test(filename)) return;
16241
16284
  if (!isExportedGetHandler(node)) return;
@@ -17014,7 +17057,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
17014
17057
  let suspenseLocalNames = /* @__PURE__ */ new Set();
17015
17058
  return {
17016
17059
  Program(programNode) {
17017
- const filename = normalizeFilename$1(context.filename ?? "");
17060
+ const filename = normalizeFilename(context.filename ?? "");
17018
17061
  isPageOrLayoutFile = PAGE_OR_LAYOUT_FILE_PATTERN.test(filename);
17019
17062
  if (!isPageOrLayoutFile) return;
17020
17063
  hasAncestorLayoutSuspense = hasAncestorSuspenseLayout(context.filename ?? "");
@@ -17386,11 +17429,19 @@ const isSynchronous = (node, within) => {
17386
17429
  if (isNodeOfType(node, "AwaitExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "void" || isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")) return false;
17387
17430
  return isSynchronous(node.parent, within);
17388
17431
  };
17432
+ const unwrapUseCallback = (node) => {
17433
+ if (!node || !isNodeOfType(node, "CallExpression")) return node;
17434
+ const callee = node.callee;
17435
+ return isNodeOfType(callee, "Identifier") && callee.name === "useCallback" || isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useCallback" ? node.arguments?.[0] : node;
17436
+ };
17389
17437
  const resolveToFunction = (ref) => {
17390
17438
  const definitionNode = ref.resolved?.defs[0]?.node;
17391
17439
  if (!definitionNode) return null;
17392
17440
  if (isFunctionLike$1(definitionNode)) return definitionNode;
17393
- if (isNodeOfType(definitionNode, "VariableDeclarator") && isFunctionLike$1(definitionNode.init)) return definitionNode.init;
17441
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) {
17442
+ const initializer = unwrapUseCallback(definitionNode.init);
17443
+ if (isFunctionLike$1(initializer)) return initializer;
17444
+ }
17394
17445
  return null;
17395
17446
  };
17396
17447
  const resolvesToAsyncFunction = (ref) => Boolean(resolveToFunction(ref)?.async);
@@ -18658,40 +18709,13 @@ const classifyPackagePlatform = (filename) => {
18658
18709
  return result;
18659
18710
  };
18660
18711
  //#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
18712
  //#region src/plugin/utils/is-react-native-file.ts
18689
18713
  const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
18690
18714
  const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
18691
18715
  const classifyReactNativeFileTarget = (context) => {
18692
18716
  const rawFilename = context.filename;
18693
18717
  if (!rawFilename) return "unknown";
18694
- const filename = normalizeFilename$1(rawFilename);
18718
+ const filename = normalizeFilename(rawFilename);
18695
18719
  if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
18696
18720
  if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
18697
18721
  const packagePlatform = classifyPackagePlatform(filename);
@@ -18748,7 +18772,7 @@ const noBarrelImport = defineRule({
18748
18772
  if (didReportForFile) return;
18749
18773
  const source = node.source?.value;
18750
18774
  if (typeof source !== "string" || !source.startsWith(".")) return;
18751
- const filename = normalizeFilename$1(context.filename ?? "");
18775
+ const filename = normalizeFilename(context.filename ?? "");
18752
18776
  if (!filename) return;
18753
18777
  const importRequests = getRuntimeImportRequests(node);
18754
18778
  if (importRequests.length === 0) return;
@@ -26219,15 +26243,6 @@ const CLIENT_APP_DIRECTORY_FRAMEWORKS = new Set([
26219
26243
  "vite"
26220
26244
  ]);
26221
26245
  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
26246
  const getClassifiablePathSegments = (pathSegments) => {
26232
26247
  const srcIndex = pathSegments.lastIndexOf("src");
26233
26248
  if (srcIndex === -1) return pathSegments;
@@ -26289,7 +26304,6 @@ const tokenizeIdentifierWords = (identifierName) => {
26289
26304
  const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
26290
26305
  //#endregion
26291
26306
  //#region src/plugin/constants/tanstack.ts
26292
- const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
26293
26307
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
26294
26308
  const TANSTACK_ROUTE_PROPERTY_ORDER = [
26295
26309
  "params",
@@ -26401,7 +26415,7 @@ const noSecretsInClientCode = defineRule({
26401
26415
  severity: "warn",
26402
26416
  recommendation: "Move secrets to server-only code. Anything in client env variables gets shipped to the browser, so it can't hold secrets.",
26403
26417
  create: (context) => {
26404
- const filename = normalizeFilename$1(context.filename ?? "");
26418
+ const filename = normalizeFilename(context.filename ?? "");
26405
26419
  const framework = getReactDoctorStringSetting(context.settings, "framework");
26406
26420
  const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
26407
26421
  let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
@@ -29567,7 +29581,7 @@ const onlyExportComponents = defineRule({
29567
29581
  allowConstantExport: settings.allowConstantExport
29568
29582
  };
29569
29583
  return { Program(node) {
29570
- if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
29584
+ if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
29571
29585
  const allNodes = collectAllNodes(node);
29572
29586
  const exports = [];
29573
29587
  let hasReactExport = false;
@@ -32001,6 +32015,7 @@ const renderingHydrationMismatchTime = defineRule({
32001
32015
  });
32002
32016
  //#endregion
32003
32017
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
32018
+ const USE_EFFECT_ONLY = new Set(["useEffect"]);
32004
32019
  const renderingHydrationNoFlicker = defineRule({
32005
32020
  id: "rendering-hydration-no-flicker",
32006
32021
  title: "useEffect setState flashes on mount",
@@ -32008,7 +32023,7 @@ const renderingHydrationNoFlicker = defineRule({
32008
32023
  severity: "warn",
32009
32024
  recommendation: "Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the element",
32010
32025
  create: (context) => ({ CallExpression(node) {
32011
- if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
32026
+ if (!isHookCall$1(node, USE_EFFECT_ONLY) || (node.arguments?.length ?? 0) < 2) return;
32012
32027
  const depsNode = node.arguments[1];
32013
32028
  if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
32014
32029
  const callback = getEffectCallback(node);
@@ -32081,7 +32096,7 @@ const renderingSvgPrecision = defineRule({
32081
32096
  recommendation: "Round path, points, and transform decimals to 1 or 2 digits. The extra precision adds bytes with no visible difference.",
32082
32097
  create: (context) => {
32083
32098
  const filename = context.filename;
32084
- const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
32099
+ const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename(filename) : void 0);
32085
32100
  return { JSXAttribute(node) {
32086
32101
  if (isAutoGenerated) return;
32087
32102
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -33231,7 +33246,7 @@ const rnDetoxMissingAwait = defineRule({
33231
33246
  severity: "warn",
33232
33247
  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
33248
  create: (context) => {
33234
- const filename = normalizeFilename$1(context.filename ?? "");
33249
+ const filename = normalizeFilename(context.filename ?? "");
33235
33250
  if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$2;
33236
33251
  return { ExpressionStatement(node) {
33237
33252
  const expression = node.expression;
@@ -34961,7 +34976,7 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
34961
34976
  //#region src/plugin/utils/is-expo-managed-file.ts
34962
34977
  const isExpoManagedFileActive = (context) => {
34963
34978
  const rawFilename = context.filename;
34964
- const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
34979
+ const filename = rawFilename ? normalizeFilename(rawFilename) : void 0;
34965
34980
  if (filename) {
34966
34981
  const packagePlatform = classifyPackagePlatform(filename);
34967
34982
  if (packagePlatform === "expo") return true;
@@ -35735,6 +35750,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
35735
35750
  "aria-labelledby",
35736
35751
  "aria-live",
35737
35752
  "aria-owns",
35753
+ "aria-readonly",
35738
35754
  "aria-relevant",
35739
35755
  "aria-required",
35740
35756
  "aria-roledescription"
@@ -35785,6 +35801,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
35785
35801
  "aria-labelledby",
35786
35802
  "aria-live",
35787
35803
  "aria-owns",
35804
+ "aria-readonly",
35788
35805
  "aria-relevant",
35789
35806
  "aria-required",
35790
35807
  "aria-roledescription",
@@ -35816,6 +35833,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
35816
35833
  "aria-labelledby",
35817
35834
  "aria-live",
35818
35835
  "aria-owns",
35836
+ "aria-readonly",
35819
35837
  "aria-relevant",
35820
35838
  "aria-required",
35821
35839
  "aria-roledescription"
@@ -37114,7 +37132,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37114
37132
  "aria-label",
37115
37133
  "aria-labelledby",
37116
37134
  "aria-live",
37135
+ "aria-multiselectable",
37117
37136
  "aria-owns",
37137
+ "aria-readonly",
37118
37138
  "aria-relevant",
37119
37139
  "aria-roledescription",
37120
37140
  "aria-rowcount"
@@ -37142,6 +37162,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37142
37162
  "aria-labelledby",
37143
37163
  "aria-live",
37144
37164
  "aria-owns",
37165
+ "aria-readonly",
37145
37166
  "aria-relevant",
37146
37167
  "aria-required",
37147
37168
  "aria-roledescription",
@@ -37331,8 +37352,10 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37331
37352
  "aria-label",
37332
37353
  "aria-labelledby",
37333
37354
  "aria-live",
37355
+ "aria-multiselectable",
37334
37356
  "aria-orientation",
37335
37357
  "aria-owns",
37358
+ "aria-readonly",
37336
37359
  "aria-relevant",
37337
37360
  "aria-required",
37338
37361
  "aria-roledescription"
@@ -37548,6 +37571,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37548
37571
  "aria-live",
37549
37572
  "aria-owns",
37550
37573
  "aria-posinset",
37574
+ "aria-readonly",
37551
37575
  "aria-relevant",
37552
37576
  "aria-required",
37553
37577
  "aria-roledescription",
@@ -37576,6 +37600,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37576
37600
  "aria-live",
37577
37601
  "aria-owns",
37578
37602
  "aria-posinset",
37603
+ "aria-readonly",
37579
37604
  "aria-relevant",
37580
37605
  "aria-required",
37581
37606
  "aria-roledescription",
@@ -37778,6 +37803,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37778
37803
  "aria-live",
37779
37804
  "aria-orientation",
37780
37805
  "aria-owns",
37806
+ "aria-readonly",
37781
37807
  "aria-relevant",
37782
37808
  "aria-required",
37783
37809
  "aria-roledescription"
@@ -37912,6 +37938,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37912
37938
  "aria-labelledby",
37913
37939
  "aria-live",
37914
37940
  "aria-owns",
37941
+ "aria-readonly",
37915
37942
  "aria-relevant",
37916
37943
  "aria-required",
37917
37944
  "aria-roledescription",
@@ -37988,6 +38015,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
37988
38015
  "aria-multiline",
37989
38016
  "aria-owns",
37990
38017
  "aria-placeholder",
38018
+ "aria-readonly",
37991
38019
  "aria-relevant",
37992
38020
  "aria-required",
37993
38021
  "aria-roledescription"
@@ -38098,10 +38126,11 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38098
38126
  "aria-live",
38099
38127
  "aria-orientation",
38100
38128
  "aria-owns",
38129
+ "aria-readonly",
38101
38130
  "aria-relevant",
38102
38131
  "aria-roledescription",
38103
- "aria-valuemin",
38104
38132
  "aria-valuemax",
38133
+ "aria-valuemin",
38105
38134
  "aria-valuenow",
38106
38135
  "aria-valuetext"
38107
38136
  ]),
@@ -38125,6 +38154,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38125
38154
  "aria-labelledby",
38126
38155
  "aria-live",
38127
38156
  "aria-owns",
38157
+ "aria-readonly",
38128
38158
  "aria-relevant",
38129
38159
  "aria-required",
38130
38160
  "aria-roledescription",
@@ -38258,6 +38288,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38258
38288
  "aria-labelledby",
38259
38289
  "aria-live",
38260
38290
  "aria-owns",
38291
+ "aria-readonly",
38261
38292
  "aria-relevant",
38262
38293
  "aria-required",
38263
38294
  "aria-roledescription"
@@ -38326,6 +38357,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38326
38357
  "aria-labelledby",
38327
38358
  "aria-level",
38328
38359
  "aria-live",
38360
+ "aria-multiselectable",
38329
38361
  "aria-orientation",
38330
38362
  "aria-owns",
38331
38363
  "aria-relevant",
@@ -38393,6 +38425,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38393
38425
  "aria-multiline",
38394
38426
  "aria-owns",
38395
38427
  "aria-placeholder",
38428
+ "aria-readonly",
38396
38429
  "aria-relevant",
38397
38430
  "aria-required",
38398
38431
  "aria-roledescription"
@@ -38495,6 +38528,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38495
38528
  "aria-label",
38496
38529
  "aria-labelledby",
38497
38530
  "aria-live",
38531
+ "aria-multiselectable",
38498
38532
  "aria-orientation",
38499
38533
  "aria-owns",
38500
38534
  "aria-relevant",
@@ -38512,6 +38546,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38512
38546
  "aria-details",
38513
38547
  "aria-disabled",
38514
38548
  "aria-dropeffect",
38549
+ "aria-errormessage",
38515
38550
  "aria-flowto",
38516
38551
  "aria-grabbed",
38517
38552
  "aria-hidden",
@@ -38520,8 +38555,10 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
38520
38555
  "aria-label",
38521
38556
  "aria-labelledby",
38522
38557
  "aria-live",
38558
+ "aria-multiselectable",
38523
38559
  "aria-orientation",
38524
38560
  "aria-owns",
38561
+ "aria-readonly",
38525
38562
  "aria-relevant",
38526
38563
  "aria-required",
38527
38564
  "aria-roledescription",
@@ -39553,8 +39590,8 @@ const serverFetchWithoutRevalidate = defineRule({
39553
39590
  let isServerSideFile = false;
39554
39591
  return {
39555
39592
  Program(node) {
39556
- const filename = normalizeFilename$1(context.filename ?? "");
39557
- if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
39593
+ const filename = normalizeFilename(context.filename ?? "");
39594
+ if (!isInProjectDirectory(context, "app") || !APP_ROUTER_FILE_PATTERN.test(filename)) {
39558
39595
  isServerSideFile = false;
39559
39596
  return;
39560
39597
  }
@@ -39639,7 +39676,6 @@ const collectRequestTaintedNames = (handlerBody, handlerParamNames) => {
39639
39676
  });
39640
39677
  return taintedNames;
39641
39678
  };
39642
- const PAGES_ROUTER_API_PATH_PATTERN = /\/pages\/api\//;
39643
39679
  const inspectHandlerBody = (context, handlerBody, handlerLabel, handlerParamNames) => {
39644
39680
  const requestTaintedNames = collectRequestTaintedNames(handlerBody, handlerParamNames);
39645
39681
  walkAst(handlerBody, (child) => {
@@ -39680,8 +39716,7 @@ const serverHoistStaticIo = defineRule({
39680
39716
  inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectHandlerParamNames(declaration.params ?? []));
39681
39717
  },
39682
39718
  ExportDefaultDeclaration(node) {
39683
- const filename = normalizeFilename$1(context.filename ?? "");
39684
- if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
39719
+ if (!isInProjectDirectory(context, "pages/api")) return;
39685
39720
  const declaration = node.declaration;
39686
39721
  if (!isFunctionLike$1(declaration)) return;
39687
39722
  if (!declaration.async) return;
@@ -40735,7 +40770,7 @@ const tanstackStartMissingHeadContent = defineRule({
40735
40770
  collectVariableAlias(node);
40736
40771
  },
40737
40772
  JSXOpeningElement(node) {
40738
- const filename = normalizeFilename$1(context.filename ?? "");
40773
+ const filename = normalizeFilename(context.filename ?? "");
40739
40774
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40740
40775
  if (isNodeOfType(node.name, "JSXIdentifier")) {
40741
40776
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
@@ -40750,7 +40785,7 @@ const tanstackStartMissingHeadContent = defineRule({
40750
40785
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
40751
40786
  },
40752
40787
  "Program:exit"(programNode) {
40753
- const filename = normalizeFilename$1(context.filename ?? "");
40788
+ const filename = normalizeFilename(context.filename ?? "");
40754
40789
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40755
40790
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
40756
40791
  node: programNode,
@@ -40776,8 +40811,7 @@ const tanstackStartNoAnchorElement = defineRule({
40776
40811
  severity: "warn",
40777
40812
  recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
40778
40813
  create: (context) => ({ JSXOpeningElement(node) {
40779
- const filename = normalizeFilename$1(context.filename ?? "");
40780
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40814
+ if (!isInProjectDirectory(context, "routes")) return;
40781
40815
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40782
40816
  const attributes = node.attributes ?? [];
40783
40817
  const hrefAttribute = findJsxAttribute(attributes, "href");
@@ -40860,6 +40894,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40860
40894
  severity: "warn",
40861
40895
  recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
40862
40896
  create: (context) => {
40897
+ const isRouteFile = isInProjectDirectory(context, "routes");
40863
40898
  let deferredCallbackDepth = 0;
40864
40899
  let eventHandlerDepth = 0;
40865
40900
  const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
@@ -40923,8 +40958,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40923
40958
  };
40924
40959
  return {
40925
40960
  CallExpression(node) {
40926
- const filename = normalizeFilename$1(context.filename ?? "");
40927
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40961
+ if (!isRouteFile) return;
40928
40962
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
40929
40963
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
40930
40964
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
@@ -40936,48 +40970,39 @@ const tanstackStartNoNavigateInRender = defineRule({
40936
40970
  }
40937
40971
  },
40938
40972
  "CallExpression:exit"(node) {
40939
- const filename = normalizeFilename$1(context.filename ?? "");
40940
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40973
+ if (!isRouteFile) return;
40941
40974
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
40942
40975
  },
40943
40976
  JSXAttribute(node) {
40944
- const filename = normalizeFilename$1(context.filename ?? "");
40945
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40977
+ if (!isRouteFile) return;
40946
40978
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
40947
40979
  },
40948
40980
  "JSXAttribute:exit"(node) {
40949
- const filename = normalizeFilename$1(context.filename ?? "");
40950
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40981
+ if (!isRouteFile) return;
40951
40982
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40952
40983
  },
40953
40984
  Property(node) {
40954
- const filename = normalizeFilename$1(context.filename ?? "");
40955
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40985
+ if (!isRouteFile) return;
40956
40986
  if (isEventHandlerProperty(node)) eventHandlerDepth++;
40957
40987
  },
40958
40988
  "Property:exit"(node) {
40959
- const filename = normalizeFilename$1(context.filename ?? "");
40960
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40989
+ if (!isRouteFile) return;
40961
40990
  if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40962
40991
  },
40963
40992
  VariableDeclarator(node) {
40964
- const filename = normalizeFilename$1(context.filename ?? "");
40965
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40993
+ if (!isRouteFile) return;
40966
40994
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
40967
40995
  },
40968
40996
  "VariableDeclarator:exit"(node) {
40969
- const filename = normalizeFilename$1(context.filename ?? "");
40970
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
40997
+ if (!isRouteFile) return;
40971
40998
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40972
40999
  },
40973
41000
  FunctionDeclaration(node) {
40974
- const filename = normalizeFilename$1(context.filename ?? "");
40975
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
41001
+ if (!isRouteFile) return;
40976
41002
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
40977
41003
  },
40978
41004
  "FunctionDeclaration:exit"(node) {
40979
- const filename = normalizeFilename$1(context.filename ?? "");
40980
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
41005
+ if (!isRouteFile) return;
40981
41006
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40982
41007
  }
40983
41008
  };
@@ -41062,8 +41087,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
41062
41087
  severity: "warn",
41063
41088
  recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
41064
41089
  create: (context) => ({ CallExpression(node) {
41065
- const filename = normalizeFilename$1(context.filename ?? "");
41066
- if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
41090
+ if (!isInProjectDirectory(context, "routes")) return;
41067
41091
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41068
41092
  const callback = node.arguments?.[0];
41069
41093
  if (!callback) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.0-dev.113637e",
3
+ "version": "0.6.0-dev.5f60bef",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",