oxlint-plugin-react-doctor 0.5.1-dev.2f26228 → 0.5.1-dev.3de9106

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 +532 -303
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1132,6 +1132,15 @@ const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(no
1132
1132
  const NEXTJS_SOURCE_FILE_EXTENSION_GROUP = "(?:tsx?|jsx?|mts|mjs)";
1133
1133
  const PAGE_FILE_PATTERN = new RegExp(`/page\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1134
1134
  const PAGE_OR_LAYOUT_FILE_PATTERN = new RegExp(`/(page|layout)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1135
+ const LAYOUT_FILE_NAMES = [
1136
+ "layout.tsx",
1137
+ "layout.jsx",
1138
+ "layout.ts",
1139
+ "layout.js",
1140
+ "layout.mts",
1141
+ "layout.mjs"
1142
+ ];
1143
+ const METADATA_EXPORT_NAMES = ["metadata", "generateMetadata"];
1135
1144
  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;
1136
1145
  const PAGES_DIRECTORY_PATTERN = /\/pages\//;
1137
1146
  const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
@@ -5830,6 +5839,20 @@ const isReactHookName = (name) => {
5830
5839
  //#region src/plugin/utils/is-react-component-or-hook-name.ts
5831
5840
  const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
5832
5841
  //#endregion
5842
+ //#region src/plugin/utils/is-react-hoc-callback-argument.ts
5843
+ const reactHocCalleeName = (callee) => {
5844
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
5845
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "React" && isNodeOfType(callee.property, "Identifier")) return `React.${callee.property.name}`;
5846
+ return null;
5847
+ };
5848
+ const isReactHocCallbackArgument = (functionNode) => {
5849
+ const parent = functionNode.parent;
5850
+ if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
5851
+ if (parent.arguments[0] !== functionNode) return false;
5852
+ const calleeName = reactHocCalleeName(parent.callee);
5853
+ return calleeName !== null && REACT_HOC_NAMES.has(calleeName);
5854
+ };
5855
+ //#endregion
5833
5856
  //#region src/plugin/rules/react-builtins/exhaustive-deps-low-level.ts
5834
5857
  /**
5835
5858
  * Lowest-level helpers consumed by both the main `exhaustive-deps`
@@ -6060,6 +6083,7 @@ const findEnclosingComponentOrHookFunction$1 = (node) => {
6060
6083
  let current = node.parent;
6061
6084
  while (current) {
6062
6085
  if (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression")) {
6086
+ if (isReactHocCallbackArgument(current)) return current;
6063
6087
  const functionName = inferFunctionName$1(current);
6064
6088
  if (functionName && isReactComponentOrHookName(functionName)) return current;
6065
6089
  }
@@ -12543,6 +12567,64 @@ const nextjsInlineScriptMissingId = defineRule({
12543
12567
  } })
12544
12568
  });
12545
12569
  //#endregion
12570
+ //#region src/plugin/utils/parse-export-specifiers.ts
12571
+ const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
12572
+ const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
12573
+ const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
12574
+ const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
12575
+ const localName = getSpecifierName(rawLocalName ?? "");
12576
+ return {
12577
+ localName,
12578
+ exportedName: getSpecifierName(rawExportedName ?? localName),
12579
+ isTypeOnly
12580
+ };
12581
+ });
12582
+ //#endregion
12583
+ //#region src/plugin/utils/strip-js-comments.ts
12584
+ const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
12585
+ const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
12586
+ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
12587
+ //#endregion
12588
+ //#region src/plugin/utils/does-module-export-name.ts
12589
+ const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
12590
+ const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
12591
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
12592
+ const doesSourceTextExportName = (sourceText, exportedName) => {
12593
+ const strippedSource = stripJsComments(sourceText);
12594
+ if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
12595
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
12596
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
12597
+ return false;
12598
+ };
12599
+ const doesModuleExportName = (filePath, exportedName) => {
12600
+ try {
12601
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
12602
+ } catch {
12603
+ return false;
12604
+ }
12605
+ };
12606
+ //#endregion
12607
+ //#region src/plugin/utils/has-ancestor-layout-matching.ts
12608
+ const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
12609
+ const normalizedPage = pageFilename.replaceAll("\\", "/");
12610
+ let currentDirectory = path.dirname(normalizedPage);
12611
+ for (let level = 0; level < 30; level++) {
12612
+ for (const layoutFileName of LAYOUT_FILE_NAMES) {
12613
+ const layoutPath = path.join(currentDirectory, layoutFileName);
12614
+ if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
12615
+ if (matchesLayout(layoutPath)) return true;
12616
+ }
12617
+ const parentDirectory = path.dirname(currentDirectory);
12618
+ if (path.basename(currentDirectory) === "app" && path.basename(parentDirectory) !== "app") break;
12619
+ if (parentDirectory === currentDirectory) break;
12620
+ currentDirectory = parentDirectory;
12621
+ }
12622
+ return false;
12623
+ };
12624
+ //#endregion
12625
+ //#region src/plugin/utils/find-ancestor-metadata-layout.ts
12626
+ const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
12627
+ //#endregion
12546
12628
  //#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
12547
12629
  const nextjsMissingMetadata = defineRule({
12548
12630
  id: "nextjs-missing-metadata",
@@ -12555,13 +12637,15 @@ const nextjsMissingMetadata = defineRule({
12555
12637
  const filename = normalizeFilename$1(context.filename ?? "");
12556
12638
  if (!PAGE_FILE_PATTERN.test(filename)) return;
12557
12639
  if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
12558
- if (!programNode.body?.some((statement) => {
12640
+ if (programNode.body?.some((statement) => {
12559
12641
  if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
12560
12642
  const declaration = statement.declaration;
12561
- if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && (declarator.id.name === "metadata" || declarator.id.name === "generateMetadata"));
12643
+ if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && METADATA_EXPORT_NAMES.includes(declarator.id.name));
12562
12644
  if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
12563
- return false;
12564
- })) context.report({
12645
+ return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
12646
+ })) return;
12647
+ if (hasAncestorMetadataLayout(context.filename ?? "")) return;
12648
+ context.report({
12565
12649
  node: programNode,
12566
12650
  message: "This page has no metadata, so search engines and social previews get no title or description."
12567
12651
  });
@@ -13452,43 +13536,6 @@ const parseSourceFile = (absoluteFilePath) => {
13452
13536
  return parsedProgram;
13453
13537
  };
13454
13538
  //#endregion
13455
- //#region src/plugin/utils/parse-export-specifiers.ts
13456
- const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
13457
- const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
13458
- const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
13459
- const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
13460
- const localName = getSpecifierName(rawLocalName ?? "");
13461
- return {
13462
- localName,
13463
- exportedName: getSpecifierName(rawExportedName ?? localName),
13464
- isTypeOnly
13465
- };
13466
- });
13467
- //#endregion
13468
- //#region src/plugin/utils/strip-js-comments.ts
13469
- const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
13470
- const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
13471
- const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
13472
- //#endregion
13473
- //#region src/plugin/utils/does-module-export-name.ts
13474
- const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
13475
- const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
13476
- const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
13477
- const doesSourceTextExportName = (sourceText, exportedName) => {
13478
- const strippedSource = stripJsComments(sourceText);
13479
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
13480
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
13481
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
13482
- return false;
13483
- };
13484
- const doesModuleExportName = (filePath, exportedName) => {
13485
- try {
13486
- return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
13487
- } catch {
13488
- return false;
13489
- }
13490
- };
13491
- //#endregion
13492
13539
  //#region src/plugin/utils/is-barrel-index-module.ts
13493
13540
  const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
13494
13541
  const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
@@ -13972,29 +14019,10 @@ const astMentionsSuspense = (programNode) => {
13972
14019
  };
13973
14020
  //#endregion
13974
14021
  //#region src/plugin/utils/find-ancestor-suspense-layout.ts
13975
- const LAYOUT_FILE_NAMES = [
13976
- "layout.tsx",
13977
- "layout.jsx",
13978
- "layout.ts",
13979
- "layout.js"
13980
- ];
13981
- const hasAncestorSuspenseLayout = (pageFilename) => {
13982
- const normalizedPage = pageFilename.replaceAll("\\", "/");
13983
- let currentDirectory = path.dirname(normalizedPage);
13984
- for (let level = 0; level < 30; level++) {
13985
- for (const layoutFileName of LAYOUT_FILE_NAMES) {
13986
- const layoutPath = path.join(currentDirectory, layoutFileName);
13987
- if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
13988
- const programRoot = parseSourceFile(layoutPath);
13989
- if (programRoot && astMentionsSuspense(programRoot)) return true;
13990
- }
13991
- if (path.basename(currentDirectory) === "app") break;
13992
- const parentDirectory = path.dirname(currentDirectory);
13993
- if (parentDirectory === currentDirectory) break;
13994
- currentDirectory = parentDirectory;
13995
- }
13996
- return false;
13997
- };
14022
+ const hasAncestorSuspenseLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => {
14023
+ const programRoot = parseSourceFile(layoutPath);
14024
+ return Boolean(programRoot && astMentionsSuspense(programRoot));
14025
+ });
13998
14026
  //#endregion
13999
14027
  //#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
14000
14028
  const astContainsUseSearchParams = (root) => {
@@ -15324,6 +15352,182 @@ const createRelativeImportSource = (filename, targetFilePath) => {
15324
15352
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
15325
15353
  };
15326
15354
  //#endregion
15355
+ //#region src/react-native-dependency-names.ts
15356
+ const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
15357
+ "expo",
15358
+ "expo-router",
15359
+ "@expo/cli",
15360
+ "@expo/metro-config",
15361
+ "@expo/metro-runtime"
15362
+ ]);
15363
+ const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
15364
+ "react-native",
15365
+ "react-native-tvos",
15366
+ ...EXPO_MANAGED_DEPENDENCY_NAMES,
15367
+ "react-native-windows",
15368
+ "react-native-macos"
15369
+ ]);
15370
+ const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
15371
+ const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
15372
+ const isReactNativeDependencyName = (dependencyName) => {
15373
+ if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
15374
+ for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
15375
+ return false;
15376
+ };
15377
+ //#endregion
15378
+ //#region src/plugin/utils/classify-package-platform.ts
15379
+ const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
15380
+ "next",
15381
+ "vite",
15382
+ "react-scripts",
15383
+ "gatsby",
15384
+ "@remix-run/react",
15385
+ "@remix-run/node",
15386
+ "@docusaurus/core",
15387
+ "@docusaurus/preset-classic",
15388
+ "@storybook/react",
15389
+ "@storybook/react-vite",
15390
+ "@storybook/react-webpack5",
15391
+ "@storybook/nextjs",
15392
+ "@storybook/web-components",
15393
+ "storybook",
15394
+ "react-dom",
15395
+ "@vitejs/plugin-react",
15396
+ "@vitejs/plugin-react-swc"
15397
+ ]);
15398
+ const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
15399
+ const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
15400
+ const findNearestPackageDirectory = (filename) => {
15401
+ if (!filename) return null;
15402
+ const fromCache = cachedPackageDirectoryByFilename.get(filename);
15403
+ if (fromCache !== void 0) return fromCache;
15404
+ let currentDirectory = path.dirname(filename);
15405
+ while (true) {
15406
+ const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
15407
+ let hasPackageJson = false;
15408
+ try {
15409
+ hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
15410
+ } catch {
15411
+ hasPackageJson = false;
15412
+ }
15413
+ if (hasPackageJson) {
15414
+ cachedPackageDirectoryByFilename.set(filename, currentDirectory);
15415
+ return currentDirectory;
15416
+ }
15417
+ const parentDirectory = path.dirname(currentDirectory);
15418
+ if (parentDirectory === currentDirectory) {
15419
+ cachedPackageDirectoryByFilename.set(filename, null);
15420
+ return null;
15421
+ }
15422
+ currentDirectory = parentDirectory;
15423
+ }
15424
+ };
15425
+ const readPackageJsonSafe = (packageJsonPath) => {
15426
+ let rawContents;
15427
+ try {
15428
+ rawContents = fs.readFileSync(packageJsonPath, "utf-8");
15429
+ } catch {
15430
+ return null;
15431
+ }
15432
+ try {
15433
+ const parsed = JSON.parse(rawContents);
15434
+ if (typeof parsed === "object" && parsed !== null) return parsed;
15435
+ return null;
15436
+ } catch {
15437
+ return null;
15438
+ }
15439
+ };
15440
+ const DEPENDENCY_SECTION_NAMES = [
15441
+ "dependencies",
15442
+ "devDependencies",
15443
+ "peerDependencies",
15444
+ "optionalDependencies"
15445
+ ];
15446
+ const iterateDependencyNames = function* (packageJson) {
15447
+ for (const sectionName of DEPENDENCY_SECTION_NAMES) {
15448
+ const section = packageJson[sectionName];
15449
+ if (!section) continue;
15450
+ for (const dependencyName of Object.keys(section)) yield dependencyName;
15451
+ }
15452
+ };
15453
+ const isReactNativeAware = (packageJson) => {
15454
+ if (typeof packageJson["react-native"] === "string") return true;
15455
+ for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
15456
+ return false;
15457
+ };
15458
+ const isExpoManaged = (packageJson) => {
15459
+ for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
15460
+ return false;
15461
+ };
15462
+ const isWebFrameworkOnly = (packageJson) => {
15463
+ for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
15464
+ return false;
15465
+ };
15466
+ const classifyPackagePlatform = (filename) => {
15467
+ const packageDirectory = findNearestPackageDirectory(filename);
15468
+ if (!packageDirectory) return "unknown";
15469
+ const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
15470
+ if (cached !== void 0) return cached;
15471
+ const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
15472
+ if (!packageJson) {
15473
+ cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
15474
+ return "unknown";
15475
+ }
15476
+ let result;
15477
+ if (isExpoManaged(packageJson)) result = "expo";
15478
+ else if (isReactNativeAware(packageJson)) result = "react-native";
15479
+ else if (isWebFrameworkOnly(packageJson)) result = "web";
15480
+ else result = "unknown";
15481
+ cachedPlatformByPackageDirectory.set(packageDirectory, result);
15482
+ return result;
15483
+ };
15484
+ //#endregion
15485
+ //#region src/plugin/utils/get-react-doctor-setting.ts
15486
+ const readReactDoctorSettingsBag = (settings) => {
15487
+ const reactDoctorSettings = settings?.["react-doctor"];
15488
+ if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
15489
+ return reactDoctorSettings;
15490
+ };
15491
+ const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
15492
+ const getReactDoctorStringSetting = (settings, settingName) => {
15493
+ const bag = readReactDoctorSettingsBag(settings);
15494
+ if (!bag) return void 0;
15495
+ const settingValue = readOwnPropertyValue(bag, settingName);
15496
+ return typeof settingValue === "string" ? settingValue : void 0;
15497
+ };
15498
+ const getReactDoctorNumberSetting = (settings, settingName) => {
15499
+ const bag = readReactDoctorSettingsBag(settings);
15500
+ if (!bag) return void 0;
15501
+ const settingValue = readOwnPropertyValue(bag, settingName);
15502
+ return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
15503
+ };
15504
+ const getReactDoctorStringArraySetting = (settings, settingName) => {
15505
+ const bag = readReactDoctorSettingsBag(settings);
15506
+ if (!bag) return [];
15507
+ const settingValue = readOwnPropertyValue(bag, settingName);
15508
+ if (!Array.isArray(settingValue)) return [];
15509
+ return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
15510
+ };
15511
+ //#endregion
15512
+ //#region src/plugin/utils/is-react-native-file.ts
15513
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
15514
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
15515
+ const classifyReactNativeFileTarget = (context) => {
15516
+ const rawFilename = context.filename;
15517
+ if (!rawFilename) return "unknown";
15518
+ const filename = normalizeFilename$1(rawFilename);
15519
+ if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
15520
+ if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
15521
+ const packagePlatform = classifyPackagePlatform(filename);
15522
+ if (packagePlatform === "web") return "web";
15523
+ if (packagePlatform === "expo" || packagePlatform === "react-native") return "react-native";
15524
+ const framework = getReactDoctorStringSetting(context.settings, "framework");
15525
+ if (framework === "react-native" || framework === "expo") return "react-native";
15526
+ if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return "web";
15527
+ return "unknown";
15528
+ };
15529
+ const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
15530
+ //#endregion
15327
15531
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
15328
15532
  const getLiteralName = (node) => {
15329
15533
  if (node.type === "Identifier" && typeof node.name === "string") return node.name;
@@ -15341,7 +15545,8 @@ const getRuntimeImportRequests = (node) => {
15341
15545
  return [{ importedName: null }];
15342
15546
  });
15343
15547
  };
15344
- const buildReportMessage = (filename, barrelFilePath, importRequests) => {
15548
+ const buildReportMessage = (filename, barrelFilePath, importRequests, isReactNativeTarget) => {
15549
+ const costSentence = isReactNativeTarget ? "This ships extra code in your app bundle & slows startup." : "This ships extra code to your users & slows page load.";
15345
15550
  const directImportSources = /* @__PURE__ */ new Set();
15346
15551
  for (const request of importRequests) {
15347
15552
  if (!request.importedName) continue;
@@ -15350,9 +15555,9 @@ const buildReportMessage = (filename, barrelFilePath, importRequests) => {
15350
15555
  }
15351
15556
  if (directImportSources.size === 1) {
15352
15557
  const [directImportSource] = directImportSources;
15353
- return `This ships extra code to your users & slows page load. Import directly from "${directImportSource}".`;
15558
+ return `${costSentence} Import directly from "${directImportSource}".`;
15354
15559
  }
15355
- if (directImportSources.size > 1) return `This ships extra code to your users & slows page load. Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
15560
+ if (directImportSources.size > 1) return `${costSentence} Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
15356
15561
  return "Importing from an index file pulls in extra code. Import directly from the source file instead.";
15357
15562
  };
15358
15563
  const noBarrelImport = defineRule({
@@ -15376,7 +15581,7 @@ const noBarrelImport = defineRule({
15376
15581
  didReportForFile = true;
15377
15582
  context.report({
15378
15583
  node,
15379
- message: buildReportMessage(filename, resolvedImportPath, importRequests)
15584
+ message: buildReportMessage(filename, resolvedImportPath, importRequests, classifyReactNativeFileTarget(context) === "react-native")
15380
15585
  });
15381
15586
  }
15382
15587
  } };
@@ -20669,33 +20874,6 @@ const noPolymorphicChildren = defineRule({
20669
20874
  } })
20670
20875
  });
20671
20876
  //#endregion
20672
- //#region src/plugin/utils/get-react-doctor-setting.ts
20673
- const readReactDoctorSettingsBag = (settings) => {
20674
- const reactDoctorSettings = settings?.["react-doctor"];
20675
- if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
20676
- return reactDoctorSettings;
20677
- };
20678
- const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
20679
- const getReactDoctorStringSetting = (settings, settingName) => {
20680
- const bag = readReactDoctorSettingsBag(settings);
20681
- if (!bag) return void 0;
20682
- const settingValue = readOwnPropertyValue(bag, settingName);
20683
- return typeof settingValue === "string" ? settingValue : void 0;
20684
- };
20685
- const getReactDoctorNumberSetting = (settings, settingName) => {
20686
- const bag = readReactDoctorSettingsBag(settings);
20687
- if (!bag) return void 0;
20688
- const settingValue = readOwnPropertyValue(bag, settingName);
20689
- return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
20690
- };
20691
- const getReactDoctorStringArraySetting = (settings, settingName) => {
20692
- const bag = readReactDoctorSettingsBag(settings);
20693
- if (!bag) return [];
20694
- const settingValue = readOwnPropertyValue(bag, settingName);
20695
- if (!Array.isArray(settingValue)) return [];
20696
- return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
20697
- };
20698
- //#endregion
20699
20877
  //#region src/plugin/rules/correctness/no-prevent-default.ts
20700
20878
  const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
20701
20879
  const SERVER_CAPABLE_FRAMEWORKS = new Set([
@@ -24344,6 +24522,21 @@ const noZIndex9999 = defineRule({
24344
24522
  })
24345
24523
  });
24346
24524
  //#endregion
24525
+ //#region src/plugin/utils/is-framework-route-or-special-filename.ts
24526
+ const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
24527
+ const FRAMEWORK_ROUTE_FILE_PATTERNS = [
24528
+ new RegExp(`^(page|layout|loading|error|not-found|template|default|global-error|route|_app|_document|_error)\\.${sourceFileExtensionGroup}$`),
24529
+ new RegExp(`^(_layout|\\+html|\\+not-found|\\+native-intent)\\.${sourceFileExtensionGroup}$`),
24530
+ new RegExp(`(?:^__root|\\.lazy)\\.${sourceFileExtensionGroup}$`),
24531
+ new RegExp(`^(root|entry\\.client|entry\\.server)\\.${sourceFileExtensionGroup}$`)
24532
+ ];
24533
+ const isFrameworkRouteOrSpecialFilename = (rawFilename) => {
24534
+ if (!rawFilename) return false;
24535
+ if (isNextjsMetadataImageRouteFilename(rawFilename)) return true;
24536
+ const basename = path.basename(rawFilename);
24537
+ return FRAMEWORK_ROUTE_FILE_PATTERNS.some((pattern) => pattern.test(basename));
24538
+ };
24539
+ //#endregion
24347
24540
  //#region src/plugin/rules/react-builtins/only-export-components-tables.ts
24348
24541
  const NOT_REACT_COMPONENT_EXPRESSION_TYPES = new Set([
24349
24542
  "ArrayExpression",
@@ -24395,32 +24588,6 @@ const ENTRY_POINT_BASENAMES = new Set([
24395
24588
  "client.jsx",
24396
24589
  "server.tsx",
24397
24590
  "server.jsx",
24398
- "page.tsx",
24399
- "page.jsx",
24400
- "layout.tsx",
24401
- "layout.jsx",
24402
- "loading.tsx",
24403
- "loading.jsx",
24404
- "error.tsx",
24405
- "error.jsx",
24406
- "not-found.tsx",
24407
- "not-found.jsx",
24408
- "template.tsx",
24409
- "template.jsx",
24410
- "default.tsx",
24411
- "default.jsx",
24412
- "global-error.tsx",
24413
- "global-error.jsx",
24414
- "route.tsx",
24415
- "route.jsx",
24416
- "_layout.tsx",
24417
- "_layout.jsx",
24418
- "_app.tsx",
24419
- "_app.jsx",
24420
- "_document.tsx",
24421
- "_document.jsx",
24422
- "_error.tsx",
24423
- "_error.jsx",
24424
24591
  "app.tsx",
24425
24592
  "app.jsx",
24426
24593
  "App.tsx",
@@ -24713,6 +24880,7 @@ const isFileNameAllowed = (filename, checkJS) => {
24713
24880
  if (filename.includes(".test.") || filename.includes(".spec.") || filename.includes(".cy.") || filename.includes(".stories.")) return false;
24714
24881
  for (const segment of NON_FAST_REFRESH_PATH_SEGMENTS) if (filename.includes(segment)) return false;
24715
24882
  if (isEntryPointFile(filename)) return false;
24883
+ if (isFrameworkRouteOrSpecialFilename(filename)) return false;
24716
24884
  if (isAssetOrUtilityFile(filename)) return false;
24717
24885
  if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) return true;
24718
24886
  if (checkJS && filename.endsWith(".js")) return true;
@@ -28585,29 +28753,236 @@ const isInsidePlatformOsWebBranch = (node) => {
28585
28753
  //#endregion
28586
28754
  //#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
28587
28755
  const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
28588
- const resolveReturnedRootElementName = (functionNode) => {
28756
+ const COMPONENT_WRAPPER_CALLEE_NAMES = new Set(["memo", "forwardRef"]);
28757
+ const resolveCalleeName = (callee) => {
28758
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
28759
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
28760
+ return null;
28761
+ };
28762
+ const unwrapComponentDefinition = (node) => {
28763
+ let current = stripParenExpression(node);
28764
+ while (isNodeOfType(current, "CallExpression")) {
28765
+ const calleeName = resolveCalleeName(current.callee);
28766
+ const firstArgument = current.arguments?.[0];
28767
+ if (!calleeName || !COMPONENT_WRAPPER_CALLEE_NAMES.has(calleeName) || !firstArgument) break;
28768
+ current = stripParenExpression(firstArgument);
28769
+ }
28770
+ return current;
28771
+ };
28772
+ const resolveChildrenPropertyLocalName = (property) => {
28773
+ if (!isNodeOfType(property, "Property")) return null;
28774
+ if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "children") return null;
28775
+ const value = property.value;
28776
+ if (isNodeOfType(value, "Identifier")) return value.name;
28777
+ if (isNodeOfType(value, "AssignmentPattern") && isNodeOfType(value.left, "Identifier")) return value.left.name;
28778
+ return null;
28779
+ };
28780
+ const resolveParamChildrenBindings = (functionNode) => {
28781
+ const bindings = {
28782
+ childrenNames: /* @__PURE__ */ new Set(),
28783
+ propsObjectNames: /* @__PURE__ */ new Set()
28784
+ };
28785
+ const firstParam = functionNode.params?.[0];
28786
+ if (!firstParam) return bindings;
28787
+ if (isNodeOfType(firstParam, "Identifier")) {
28788
+ bindings.propsObjectNames.add(firstParam.name);
28789
+ return bindings;
28790
+ }
28791
+ if (!isNodeOfType(firstParam, "ObjectPattern")) return bindings;
28792
+ let didDestructureChildren = false;
28793
+ let restName = null;
28794
+ for (const property of firstParam.properties ?? []) {
28795
+ if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) {
28796
+ restName = property.argument.name;
28797
+ continue;
28798
+ }
28799
+ const localName = resolveChildrenPropertyLocalName(property);
28800
+ if (localName) {
28801
+ didDestructureChildren = true;
28802
+ bindings.childrenNames.add(localName);
28803
+ }
28804
+ }
28805
+ if (restName && !didDestructureChildren) bindings.propsObjectNames.add(restName);
28806
+ return bindings;
28807
+ };
28808
+ const MAX_CHILDREN_ALIAS_PASSES = 3;
28809
+ const isPropsObjectExpression = (expression, bindings) => {
28810
+ if (!expression) return false;
28811
+ const value = stripParenExpression(expression);
28812
+ if (isNodeOfType(value, "Identifier")) return bindings.propsObjectNames.has(value.name);
28813
+ return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.object, "ThisExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "props";
28814
+ };
28815
+ const isChildrenValueExpression = (expression, bindings) => {
28816
+ if (!expression) return false;
28817
+ const value = stripParenExpression(expression);
28818
+ if (isNodeOfType(value, "Identifier")) return bindings.childrenNames.has(value.name);
28819
+ return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "children" && isPropsObjectExpression(value.object, bindings);
28820
+ };
28821
+ const collectChildrenAliases = (functionNode, bindings) => {
28589
28822
  const { body } = functionNode;
28590
- if (!body) return null;
28591
- if (!isNodeOfType(body, "BlockStatement")) return isNodeOfType(body, "JSXElement") ? resolveJsxElementName(body.openingElement) : null;
28592
- for (const statement of body.body) {
28593
- if (!isNodeOfType(statement, "ReturnStatement")) continue;
28594
- const argument = statement.argument;
28595
- if (argument && isNodeOfType(argument, "JSXElement")) return resolveJsxElementName(argument.openingElement);
28823
+ if (!body || !isNodeOfType(body, "BlockStatement")) return;
28824
+ for (let pass = 0; pass < MAX_CHILDREN_ALIAS_PASSES; pass += 1) {
28825
+ const sizeBeforePass = bindings.childrenNames.size;
28826
+ walkAst(body, (node) => {
28827
+ if (isFunctionNode(node)) return false;
28828
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return void 0;
28829
+ if (isNodeOfType(node.id, "Identifier")) {
28830
+ if (isChildrenValueExpression(node.init, bindings)) bindings.childrenNames.add(node.id.name);
28831
+ return;
28832
+ }
28833
+ if (isNodeOfType(node.id, "ObjectPattern") && isPropsObjectExpression(node.init, bindings)) for (const property of node.id.properties ?? []) {
28834
+ const localName = resolveChildrenPropertyLocalName(property);
28835
+ if (localName) bindings.childrenNames.add(localName);
28836
+ }
28837
+ });
28838
+ if (bindings.childrenNames.size === sizeBeforePass) break;
28839
+ }
28840
+ };
28841
+ const collectJsxRootsFromExpression = (expression, roots) => {
28842
+ const value = stripParenExpression(expression);
28843
+ if (isNodeOfType(value, "JSXElement") || isNodeOfType(value, "JSXFragment")) {
28844
+ roots.push(value);
28845
+ return;
28846
+ }
28847
+ if (isNodeOfType(value, "ConditionalExpression")) {
28848
+ if (value.consequent) collectJsxRootsFromExpression(value.consequent, roots);
28849
+ if (value.alternate) collectJsxRootsFromExpression(value.alternate, roots);
28850
+ return;
28851
+ }
28852
+ if (isNodeOfType(value, "LogicalExpression")) {
28853
+ if (value.left) collectJsxRootsFromExpression(value.left, roots);
28854
+ if (value.right) collectJsxRootsFromExpression(value.right, roots);
28855
+ }
28856
+ };
28857
+ const collectReturnedJsxRoots = (functionNode) => {
28858
+ const roots = [];
28859
+ const { body } = functionNode;
28860
+ if (!body) return roots;
28861
+ if (!isNodeOfType(body, "BlockStatement")) {
28862
+ collectJsxRootsFromExpression(body, roots);
28863
+ return roots;
28864
+ }
28865
+ walkAst(body, (node) => {
28866
+ if (isFunctionNode(node) && node !== functionNode) return false;
28867
+ if (isNodeOfType(node, "ReturnStatement") && node.argument) {
28868
+ collectJsxRootsFromExpression(node.argument, roots);
28869
+ return false;
28870
+ }
28871
+ });
28872
+ return roots;
28873
+ };
28874
+ const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
28875
+ const isChildrenForwardingAttribute = (attribute, bindings) => {
28876
+ if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
28877
+ return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
28878
+ };
28879
+ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElement) => {
28880
+ let didForwardIntoText = false;
28881
+ walkAst(jsxRoot, (node) => {
28882
+ if (didForwardIntoText || isFunctionNode(node)) return false;
28883
+ if (!isNodeOfType(node, "JSXElement")) return void 0;
28884
+ const elementName = resolveJsxElementName(node.openingElement);
28885
+ if (!elementName || !isTextHandlingElement(elementName)) return;
28886
+ didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
28887
+ });
28888
+ return didForwardIntoText;
28889
+ };
28890
+ const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
28891
+ const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
28892
+ let didRenderOutsideText = false;
28893
+ walkAst(jsxRoot, (node) => {
28894
+ if (didRenderOutsideText || isFunctionNode(node)) return false;
28895
+ if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
28896
+ if (isNodeOfType(node, "JSXElement")) {
28897
+ const elementName = resolveJsxElementName(node.openingElement);
28898
+ if (elementName && isTextHandlingElement(elementName)) return false;
28899
+ if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
28900
+ didRenderOutsideText = true;
28901
+ return;
28902
+ }
28903
+ }
28904
+ didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
28905
+ });
28906
+ return didRenderOutsideText;
28907
+ };
28908
+ const resolveStyledFactoryBaseName = (definitionNode) => {
28909
+ let current = stripParenExpression(definitionNode);
28910
+ while (current) {
28911
+ if (isNodeOfType(current, "TaggedTemplateExpression")) {
28912
+ current = stripParenExpression(current.tag);
28913
+ continue;
28914
+ }
28915
+ if (isNodeOfType(current, "CallExpression")) {
28916
+ const callee = stripParenExpression(current.callee);
28917
+ if (isNodeOfType(callee, "Identifier") && callee.name === "styled") {
28918
+ const baseArgument = current.arguments?.[0];
28919
+ if (!baseArgument) return null;
28920
+ const base = stripParenExpression(baseArgument);
28921
+ return isNodeOfType(base, "Identifier") ? base.name : null;
28922
+ }
28923
+ current = callee;
28924
+ continue;
28925
+ }
28926
+ if (isNodeOfType(current, "MemberExpression")) {
28927
+ if (isNodeOfType(current.object, "Identifier") && current.object.name === "styled" && isNodeOfType(current.property, "Identifier")) return current.property.name;
28928
+ current = stripParenExpression(current.object);
28929
+ continue;
28930
+ }
28931
+ return null;
28932
+ }
28933
+ return null;
28934
+ };
28935
+ const resolveClassRenderFunction = (classNode) => {
28936
+ if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
28937
+ for (const member of classNode.body?.body ?? []) {
28938
+ if (!isNodeOfType(member, "MethodDefinition")) continue;
28939
+ if (!isNodeOfType(member.key, "Identifier") || member.key.name !== "render") continue;
28940
+ return member.value && isFunctionNode(member.value) ? member.value : null;
28596
28941
  }
28597
28942
  return null;
28598
28943
  };
28599
- const recordWrapperFromDeclaration = (componentName, functionNode, isTextHandlingRoot, wrappers) => {
28944
+ const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
28600
28945
  if (!componentName || !isReactComponentName(componentName)) return;
28601
- if (!functionNode || !isFunctionNode(functionNode)) return;
28602
- const rootName = resolveReturnedRootElementName(functionNode);
28603
- if (rootName && isTextHandlingRoot(rootName)) wrappers.add(componentName);
28946
+ if (wrappers.has(componentName)) return;
28947
+ if (!definitionNode) return;
28948
+ const unwrapped = unwrapComponentDefinition(definitionNode);
28949
+ const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
28950
+ if (styledBaseName && isTextHandlingElement(styledBaseName)) {
28951
+ wrappers.add(componentName);
28952
+ return;
28953
+ }
28954
+ const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
28955
+ if (!functionNode) return;
28956
+ const bindings = resolveParamChildrenBindings(functionNode);
28957
+ collectChildrenAliases(functionNode, bindings);
28958
+ const jsxRoots = collectReturnedJsxRoots(functionNode);
28959
+ if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
28960
+ for (const jsxRoot of jsxRoots) {
28961
+ if (isNodeOfType(jsxRoot, "JSXElement")) {
28962
+ const rootName = resolveJsxElementName(jsxRoot.openingElement);
28963
+ if (rootName && isTextHandlingElement(rootName)) {
28964
+ wrappers.add(componentName);
28965
+ return;
28966
+ }
28967
+ }
28968
+ if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
28969
+ wrappers.add(componentName);
28970
+ return;
28971
+ }
28972
+ }
28604
28973
  };
28974
+ const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
28605
28975
  const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
28606
28976
  const wrappers = /* @__PURE__ */ new Set();
28607
- walkAst(programNode, (node) => {
28608
- if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingRoot, wrappers);
28609
- else if (isNodeOfType(node, "FunctionDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingRoot, wrappers);
28610
- });
28977
+ const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
28978
+ for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
28979
+ const sizeBeforePass = wrappers.size;
28980
+ walkAst(programNode, (node) => {
28981
+ if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
28982
+ else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
28983
+ });
28984
+ if (wrappers.size === sizeBeforePass) break;
28985
+ }
28611
28986
  return wrappers;
28612
28987
  };
28613
28988
  //#endregion
@@ -28675,6 +29050,7 @@ const rnNoRawText = defineRule({
28675
29050
  title: "Raw text outside a Text component",
28676
29051
  requires: ["react-native"],
28677
29052
  severity: "error",
29053
+ tags: ["test-noise"],
28678
29054
  recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
28679
29055
  create: (context) => {
28680
29056
  let isDomComponentFile = false;
@@ -28920,136 +29296,6 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
28920
29296
  recommendation: "Retired: SafeAreaView wrappers are valid; prefer native content inset adjustment only when manual inset plumbing causes scroll jumps or duplicated safe-area offsets."
28921
29297
  });
28922
29298
  //#endregion
28923
- //#region src/react-native-dependency-names.ts
28924
- const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
28925
- "expo",
28926
- "expo-router",
28927
- "@expo/cli",
28928
- "@expo/metro-config",
28929
- "@expo/metro-runtime"
28930
- ]);
28931
- const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
28932
- "react-native",
28933
- "react-native-tvos",
28934
- ...EXPO_MANAGED_DEPENDENCY_NAMES,
28935
- "react-native-windows",
28936
- "react-native-macos"
28937
- ]);
28938
- const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
28939
- const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
28940
- const isReactNativeDependencyName = (dependencyName) => {
28941
- if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
28942
- for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
28943
- return false;
28944
- };
28945
- //#endregion
28946
- //#region src/plugin/utils/classify-package-platform.ts
28947
- const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
28948
- "next",
28949
- "vite",
28950
- "react-scripts",
28951
- "gatsby",
28952
- "@remix-run/react",
28953
- "@remix-run/node",
28954
- "@docusaurus/core",
28955
- "@docusaurus/preset-classic",
28956
- "@storybook/react",
28957
- "@storybook/react-vite",
28958
- "@storybook/react-webpack5",
28959
- "@storybook/nextjs",
28960
- "@storybook/web-components",
28961
- "storybook",
28962
- "react-dom",
28963
- "@vitejs/plugin-react",
28964
- "@vitejs/plugin-react-swc"
28965
- ]);
28966
- const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
28967
- const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
28968
- const findNearestPackageDirectory = (filename) => {
28969
- if (!filename) return null;
28970
- const fromCache = cachedPackageDirectoryByFilename.get(filename);
28971
- if (fromCache !== void 0) return fromCache;
28972
- let currentDirectory = path.dirname(filename);
28973
- while (true) {
28974
- const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
28975
- let hasPackageJson = false;
28976
- try {
28977
- hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
28978
- } catch {
28979
- hasPackageJson = false;
28980
- }
28981
- if (hasPackageJson) {
28982
- cachedPackageDirectoryByFilename.set(filename, currentDirectory);
28983
- return currentDirectory;
28984
- }
28985
- const parentDirectory = path.dirname(currentDirectory);
28986
- if (parentDirectory === currentDirectory) {
28987
- cachedPackageDirectoryByFilename.set(filename, null);
28988
- return null;
28989
- }
28990
- currentDirectory = parentDirectory;
28991
- }
28992
- };
28993
- const readPackageJsonSafe = (packageJsonPath) => {
28994
- let rawContents;
28995
- try {
28996
- rawContents = fs.readFileSync(packageJsonPath, "utf-8");
28997
- } catch {
28998
- return null;
28999
- }
29000
- try {
29001
- const parsed = JSON.parse(rawContents);
29002
- if (typeof parsed === "object" && parsed !== null) return parsed;
29003
- return null;
29004
- } catch {
29005
- return null;
29006
- }
29007
- };
29008
- const DEPENDENCY_SECTION_NAMES = [
29009
- "dependencies",
29010
- "devDependencies",
29011
- "peerDependencies",
29012
- "optionalDependencies"
29013
- ];
29014
- const iterateDependencyNames = function* (packageJson) {
29015
- for (const sectionName of DEPENDENCY_SECTION_NAMES) {
29016
- const section = packageJson[sectionName];
29017
- if (!section) continue;
29018
- for (const dependencyName of Object.keys(section)) yield dependencyName;
29019
- }
29020
- };
29021
- const isReactNativeAware = (packageJson) => {
29022
- if (typeof packageJson["react-native"] === "string") return true;
29023
- for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
29024
- return false;
29025
- };
29026
- const isExpoManaged = (packageJson) => {
29027
- for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
29028
- return false;
29029
- };
29030
- const isWebFrameworkOnly = (packageJson) => {
29031
- for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
29032
- return false;
29033
- };
29034
- const classifyPackagePlatform = (filename) => {
29035
- const packageDirectory = findNearestPackageDirectory(filename);
29036
- if (!packageDirectory) return "unknown";
29037
- const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
29038
- if (cached !== void 0) return cached;
29039
- const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
29040
- if (!packageJson) {
29041
- cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
29042
- return "unknown";
29043
- }
29044
- let result;
29045
- if (isExpoManaged(packageJson)) result = "expo";
29046
- else if (isReactNativeAware(packageJson)) result = "react-native";
29047
- else if (isWebFrameworkOnly(packageJson)) result = "web";
29048
- else result = "unknown";
29049
- cachedPlatformByPackageDirectory.set(packageDirectory, result);
29050
- return result;
29051
- };
29052
- //#endregion
29053
29299
  //#region src/plugin/utils/is-expo-managed-file.ts
29054
29300
  const isExpoManagedFileActive = (context) => {
29055
29301
  const rawFilename = context.filename;
@@ -32949,7 +33195,7 @@ const findEnclosingFunctionInfo = (node) => {
32949
33195
  name: displayName,
32950
33196
  hasResolvedName: resolvedName !== null,
32951
33197
  isAsync: Boolean(current.async),
32952
- isComponentOrHook: resolvedName === null ? false : isReactComponentOrHookName(displayName)
33198
+ isComponentOrHook: isReactHocCallbackArgument(current) || (resolvedName === null ? false : isReactComponentOrHookName(displayName))
32953
33199
  };
32954
33200
  }
32955
33201
  current = current.parent ?? null;
@@ -33008,6 +33254,7 @@ const findEnclosingComponentOrHookFunction = (node) => {
33008
33254
  let current = node.parent;
33009
33255
  while (current) {
33010
33256
  if (isFunctionLike$2(current)) {
33257
+ if (isReactHocCallbackArgument(current)) return current;
33011
33258
  const resolvedName = inferFunctionName(current);
33012
33259
  if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
33013
33260
  }
@@ -33107,26 +33354,26 @@ const rulesOfHooks = defineRule({
33107
33354
  });
33108
33355
  return;
33109
33356
  }
33110
- if (!enclosing.hasResolvedName) {
33111
- let outerWalker = enclosing.node;
33112
- let outerIsComponentOrHook = false;
33113
- while (outerWalker) {
33114
- const outerInfo = findEnclosingFunctionInfo(outerWalker);
33115
- if (!outerInfo) break;
33116
- if (outerInfo.isComponentOrHook) {
33117
- outerIsComponentOrHook = true;
33118
- break;
33357
+ if (!enclosing.isComponentOrHook) {
33358
+ if (!enclosing.hasResolvedName) {
33359
+ let outerWalker = enclosing.node;
33360
+ let outerIsComponentOrHook = false;
33361
+ while (outerWalker) {
33362
+ const outerInfo = findEnclosingFunctionInfo(outerWalker);
33363
+ if (!outerInfo) break;
33364
+ if (outerInfo.isComponentOrHook) {
33365
+ outerIsComponentOrHook = true;
33366
+ break;
33367
+ }
33368
+ outerWalker = outerInfo.node;
33119
33369
  }
33120
- outerWalker = outerInfo.node;
33370
+ if (!outerIsComponentOrHook) return;
33371
+ context.report({
33372
+ node: node.callee,
33373
+ message: buildConditionalMessage(hookName)
33374
+ });
33375
+ return;
33121
33376
  }
33122
- if (!outerIsComponentOrHook) return;
33123
- context.report({
33124
- node: node.callee,
33125
- message: buildConditionalMessage(hookName)
33126
- });
33127
- return;
33128
- }
33129
- if (!enclosing.isComponentOrHook) {
33130
33377
  context.report({
33131
33378
  node: node.callee,
33132
33379
  message: buildNonComponentMessage(hookName, enclosing.name)
@@ -39085,24 +39332,6 @@ const reactDoctorRules = [
39085
39332
  ];
39086
39333
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
39087
39334
  //#endregion
39088
- //#region src/plugin/utils/is-react-native-file.ts
39089
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
39090
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
39091
- const isReactNativeFileActive = (context) => {
39092
- const rawFilename = context.filename;
39093
- if (!rawFilename) return true;
39094
- const filename = normalizeFilename$1(rawFilename);
39095
- if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
39096
- if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return false;
39097
- const packagePlatform = classifyPackagePlatform(filename);
39098
- if (packagePlatform === "web") return false;
39099
- if (packagePlatform === "expo" || packagePlatform === "react-native") return true;
39100
- const framework = getReactDoctorStringSetting(context.settings, "framework");
39101
- if (framework === "react-native" || framework === "expo") return true;
39102
- if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return false;
39103
- return true;
39104
- };
39105
- //#endregion
39106
39335
  //#region src/plugin/utils/wrap-react-native-rule.ts
39107
39336
  const EMPTY_VISITORS = {};
39108
39337
  const wrapReactNativeRule = (rule) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.1-dev.2f26228",
3
+ "version": "0.5.1-dev.3de9106",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",