oxlint-plugin-react-doctor 0.5.1-dev.f63ed9d → 0.5.1-dev.fee3fc4

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 +404 -194
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15337,6 +15337,182 @@ const createRelativeImportSource = (filename, targetFilePath) => {
15337
15337
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
15338
15338
  };
15339
15339
  //#endregion
15340
+ //#region src/react-native-dependency-names.ts
15341
+ const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
15342
+ "expo",
15343
+ "expo-router",
15344
+ "@expo/cli",
15345
+ "@expo/metro-config",
15346
+ "@expo/metro-runtime"
15347
+ ]);
15348
+ const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
15349
+ "react-native",
15350
+ "react-native-tvos",
15351
+ ...EXPO_MANAGED_DEPENDENCY_NAMES,
15352
+ "react-native-windows",
15353
+ "react-native-macos"
15354
+ ]);
15355
+ const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
15356
+ const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
15357
+ const isReactNativeDependencyName = (dependencyName) => {
15358
+ if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
15359
+ for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
15360
+ return false;
15361
+ };
15362
+ //#endregion
15363
+ //#region src/plugin/utils/classify-package-platform.ts
15364
+ const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
15365
+ "next",
15366
+ "vite",
15367
+ "react-scripts",
15368
+ "gatsby",
15369
+ "@remix-run/react",
15370
+ "@remix-run/node",
15371
+ "@docusaurus/core",
15372
+ "@docusaurus/preset-classic",
15373
+ "@storybook/react",
15374
+ "@storybook/react-vite",
15375
+ "@storybook/react-webpack5",
15376
+ "@storybook/nextjs",
15377
+ "@storybook/web-components",
15378
+ "storybook",
15379
+ "react-dom",
15380
+ "@vitejs/plugin-react",
15381
+ "@vitejs/plugin-react-swc"
15382
+ ]);
15383
+ const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
15384
+ const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
15385
+ const findNearestPackageDirectory = (filename) => {
15386
+ if (!filename) return null;
15387
+ const fromCache = cachedPackageDirectoryByFilename.get(filename);
15388
+ if (fromCache !== void 0) return fromCache;
15389
+ let currentDirectory = path.dirname(filename);
15390
+ while (true) {
15391
+ const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
15392
+ let hasPackageJson = false;
15393
+ try {
15394
+ hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
15395
+ } catch {
15396
+ hasPackageJson = false;
15397
+ }
15398
+ if (hasPackageJson) {
15399
+ cachedPackageDirectoryByFilename.set(filename, currentDirectory);
15400
+ return currentDirectory;
15401
+ }
15402
+ const parentDirectory = path.dirname(currentDirectory);
15403
+ if (parentDirectory === currentDirectory) {
15404
+ cachedPackageDirectoryByFilename.set(filename, null);
15405
+ return null;
15406
+ }
15407
+ currentDirectory = parentDirectory;
15408
+ }
15409
+ };
15410
+ const readPackageJsonSafe = (packageJsonPath) => {
15411
+ let rawContents;
15412
+ try {
15413
+ rawContents = fs.readFileSync(packageJsonPath, "utf-8");
15414
+ } catch {
15415
+ return null;
15416
+ }
15417
+ try {
15418
+ const parsed = JSON.parse(rawContents);
15419
+ if (typeof parsed === "object" && parsed !== null) return parsed;
15420
+ return null;
15421
+ } catch {
15422
+ return null;
15423
+ }
15424
+ };
15425
+ const DEPENDENCY_SECTION_NAMES = [
15426
+ "dependencies",
15427
+ "devDependencies",
15428
+ "peerDependencies",
15429
+ "optionalDependencies"
15430
+ ];
15431
+ const iterateDependencyNames = function* (packageJson) {
15432
+ for (const sectionName of DEPENDENCY_SECTION_NAMES) {
15433
+ const section = packageJson[sectionName];
15434
+ if (!section) continue;
15435
+ for (const dependencyName of Object.keys(section)) yield dependencyName;
15436
+ }
15437
+ };
15438
+ const isReactNativeAware = (packageJson) => {
15439
+ if (typeof packageJson["react-native"] === "string") return true;
15440
+ for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
15441
+ return false;
15442
+ };
15443
+ const isExpoManaged = (packageJson) => {
15444
+ for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
15445
+ return false;
15446
+ };
15447
+ const isWebFrameworkOnly = (packageJson) => {
15448
+ for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
15449
+ return false;
15450
+ };
15451
+ const classifyPackagePlatform = (filename) => {
15452
+ const packageDirectory = findNearestPackageDirectory(filename);
15453
+ if (!packageDirectory) return "unknown";
15454
+ const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
15455
+ if (cached !== void 0) return cached;
15456
+ const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
15457
+ if (!packageJson) {
15458
+ cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
15459
+ return "unknown";
15460
+ }
15461
+ let result;
15462
+ if (isExpoManaged(packageJson)) result = "expo";
15463
+ else if (isReactNativeAware(packageJson)) result = "react-native";
15464
+ else if (isWebFrameworkOnly(packageJson)) result = "web";
15465
+ else result = "unknown";
15466
+ cachedPlatformByPackageDirectory.set(packageDirectory, result);
15467
+ return result;
15468
+ };
15469
+ //#endregion
15470
+ //#region src/plugin/utils/get-react-doctor-setting.ts
15471
+ const readReactDoctorSettingsBag = (settings) => {
15472
+ const reactDoctorSettings = settings?.["react-doctor"];
15473
+ if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
15474
+ return reactDoctorSettings;
15475
+ };
15476
+ const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
15477
+ const getReactDoctorStringSetting = (settings, settingName) => {
15478
+ const bag = readReactDoctorSettingsBag(settings);
15479
+ if (!bag) return void 0;
15480
+ const settingValue = readOwnPropertyValue(bag, settingName);
15481
+ return typeof settingValue === "string" ? settingValue : void 0;
15482
+ };
15483
+ const getReactDoctorNumberSetting = (settings, settingName) => {
15484
+ const bag = readReactDoctorSettingsBag(settings);
15485
+ if (!bag) return void 0;
15486
+ const settingValue = readOwnPropertyValue(bag, settingName);
15487
+ return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
15488
+ };
15489
+ const getReactDoctorStringArraySetting = (settings, settingName) => {
15490
+ const bag = readReactDoctorSettingsBag(settings);
15491
+ if (!bag) return [];
15492
+ const settingValue = readOwnPropertyValue(bag, settingName);
15493
+ if (!Array.isArray(settingValue)) return [];
15494
+ return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
15495
+ };
15496
+ //#endregion
15497
+ //#region src/plugin/utils/is-react-native-file.ts
15498
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
15499
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
15500
+ const classifyReactNativeFileTarget = (context) => {
15501
+ const rawFilename = context.filename;
15502
+ if (!rawFilename) return "unknown";
15503
+ const filename = normalizeFilename$1(rawFilename);
15504
+ if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return "react-native";
15505
+ if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return "web";
15506
+ const packagePlatform = classifyPackagePlatform(filename);
15507
+ if (packagePlatform === "web") return "web";
15508
+ if (packagePlatform === "expo" || packagePlatform === "react-native") return "react-native";
15509
+ const framework = getReactDoctorStringSetting(context.settings, "framework");
15510
+ if (framework === "react-native" || framework === "expo") return "react-native";
15511
+ if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return "web";
15512
+ return "unknown";
15513
+ };
15514
+ const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
15515
+ //#endregion
15340
15516
  //#region src/plugin/rules/bundle-size/no-barrel-import.ts
15341
15517
  const getLiteralName = (node) => {
15342
15518
  if (node.type === "Identifier" && typeof node.name === "string") return node.name;
@@ -15354,7 +15530,8 @@ const getRuntimeImportRequests = (node) => {
15354
15530
  return [{ importedName: null }];
15355
15531
  });
15356
15532
  };
15357
- const buildReportMessage = (filename, barrelFilePath, importRequests) => {
15533
+ const buildReportMessage = (filename, barrelFilePath, importRequests, isReactNativeTarget) => {
15534
+ const costSentence = isReactNativeTarget ? "This ships extra code in your app bundle & slows startup." : "This ships extra code to your users & slows page load.";
15358
15535
  const directImportSources = /* @__PURE__ */ new Set();
15359
15536
  for (const request of importRequests) {
15360
15537
  if (!request.importedName) continue;
@@ -15363,9 +15540,9 @@ const buildReportMessage = (filename, barrelFilePath, importRequests) => {
15363
15540
  }
15364
15541
  if (directImportSources.size === 1) {
15365
15542
  const [directImportSource] = directImportSources;
15366
- return `This ships extra code to your users & slows page load. Import directly from "${directImportSource}".`;
15543
+ return `${costSentence} Import directly from "${directImportSource}".`;
15367
15544
  }
15368
- if (directImportSources.size > 1) return `This ships extra code to your users & slows page load. Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
15545
+ if (directImportSources.size > 1) return `${costSentence} Import directly from: ${[...directImportSources].map((source) => `"${source}"`).join(", ")}.`;
15369
15546
  return "Importing from an index file pulls in extra code. Import directly from the source file instead.";
15370
15547
  };
15371
15548
  const noBarrelImport = defineRule({
@@ -15389,7 +15566,7 @@ const noBarrelImport = defineRule({
15389
15566
  didReportForFile = true;
15390
15567
  context.report({
15391
15568
  node,
15392
- message: buildReportMessage(filename, resolvedImportPath, importRequests)
15569
+ message: buildReportMessage(filename, resolvedImportPath, importRequests, classifyReactNativeFileTarget(context) === "react-native")
15393
15570
  });
15394
15571
  }
15395
15572
  } };
@@ -20682,33 +20859,6 @@ const noPolymorphicChildren = defineRule({
20682
20859
  } })
20683
20860
  });
20684
20861
  //#endregion
20685
- //#region src/plugin/utils/get-react-doctor-setting.ts
20686
- const readReactDoctorSettingsBag = (settings) => {
20687
- const reactDoctorSettings = settings?.["react-doctor"];
20688
- if (typeof reactDoctorSettings !== "object" || reactDoctorSettings === null || Array.isArray(reactDoctorSettings)) return null;
20689
- return reactDoctorSettings;
20690
- };
20691
- const readOwnPropertyValue = (bag, settingName) => Object.getOwnPropertyDescriptor(bag, settingName)?.value;
20692
- const getReactDoctorStringSetting = (settings, settingName) => {
20693
- const bag = readReactDoctorSettingsBag(settings);
20694
- if (!bag) return void 0;
20695
- const settingValue = readOwnPropertyValue(bag, settingName);
20696
- return typeof settingValue === "string" ? settingValue : void 0;
20697
- };
20698
- const getReactDoctorNumberSetting = (settings, settingName) => {
20699
- const bag = readReactDoctorSettingsBag(settings);
20700
- if (!bag) return void 0;
20701
- const settingValue = readOwnPropertyValue(bag, settingName);
20702
- return typeof settingValue === "number" && Number.isFinite(settingValue) ? settingValue : void 0;
20703
- };
20704
- const getReactDoctorStringArraySetting = (settings, settingName) => {
20705
- const bag = readReactDoctorSettingsBag(settings);
20706
- if (!bag) return [];
20707
- const settingValue = readOwnPropertyValue(bag, settingName);
20708
- if (!Array.isArray(settingValue)) return [];
20709
- return settingValue.filter((entry) => typeof entry === "string" && entry.length > 0);
20710
- };
20711
- //#endregion
20712
20862
  //#region src/plugin/rules/correctness/no-prevent-default.ts
20713
20863
  const PREVENT_DEFAULT_ELEMENTS = new Map([["form", ["onSubmit"]], ["a", ["onClick"]]]);
20714
20864
  const SERVER_CAPABLE_FRAMEWORKS = new Set([
@@ -28588,29 +28738,236 @@ const isInsidePlatformOsWebBranch = (node) => {
28588
28738
  //#endregion
28589
28739
  //#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
28590
28740
  const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
28591
- const resolveReturnedRootElementName = (functionNode) => {
28741
+ const COMPONENT_WRAPPER_CALLEE_NAMES = new Set(["memo", "forwardRef"]);
28742
+ const resolveCalleeName = (callee) => {
28743
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
28744
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
28745
+ return null;
28746
+ };
28747
+ const unwrapComponentDefinition = (node) => {
28748
+ let current = stripParenExpression(node);
28749
+ while (isNodeOfType(current, "CallExpression")) {
28750
+ const calleeName = resolveCalleeName(current.callee);
28751
+ const firstArgument = current.arguments?.[0];
28752
+ if (!calleeName || !COMPONENT_WRAPPER_CALLEE_NAMES.has(calleeName) || !firstArgument) break;
28753
+ current = stripParenExpression(firstArgument);
28754
+ }
28755
+ return current;
28756
+ };
28757
+ const resolveChildrenPropertyLocalName = (property) => {
28758
+ if (!isNodeOfType(property, "Property")) return null;
28759
+ if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "children") return null;
28760
+ const value = property.value;
28761
+ if (isNodeOfType(value, "Identifier")) return value.name;
28762
+ if (isNodeOfType(value, "AssignmentPattern") && isNodeOfType(value.left, "Identifier")) return value.left.name;
28763
+ return null;
28764
+ };
28765
+ const resolveParamChildrenBindings = (functionNode) => {
28766
+ const bindings = {
28767
+ childrenNames: /* @__PURE__ */ new Set(),
28768
+ propsObjectNames: /* @__PURE__ */ new Set()
28769
+ };
28770
+ const firstParam = functionNode.params?.[0];
28771
+ if (!firstParam) return bindings;
28772
+ if (isNodeOfType(firstParam, "Identifier")) {
28773
+ bindings.propsObjectNames.add(firstParam.name);
28774
+ return bindings;
28775
+ }
28776
+ if (!isNodeOfType(firstParam, "ObjectPattern")) return bindings;
28777
+ let didDestructureChildren = false;
28778
+ let restName = null;
28779
+ for (const property of firstParam.properties ?? []) {
28780
+ if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) {
28781
+ restName = property.argument.name;
28782
+ continue;
28783
+ }
28784
+ const localName = resolveChildrenPropertyLocalName(property);
28785
+ if (localName) {
28786
+ didDestructureChildren = true;
28787
+ bindings.childrenNames.add(localName);
28788
+ }
28789
+ }
28790
+ if (restName && !didDestructureChildren) bindings.propsObjectNames.add(restName);
28791
+ return bindings;
28792
+ };
28793
+ const MAX_CHILDREN_ALIAS_PASSES = 3;
28794
+ const isPropsObjectExpression = (expression, bindings) => {
28795
+ if (!expression) return false;
28796
+ const value = stripParenExpression(expression);
28797
+ if (isNodeOfType(value, "Identifier")) return bindings.propsObjectNames.has(value.name);
28798
+ return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.object, "ThisExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "props";
28799
+ };
28800
+ const isChildrenValueExpression = (expression, bindings) => {
28801
+ if (!expression) return false;
28802
+ const value = stripParenExpression(expression);
28803
+ if (isNodeOfType(value, "Identifier")) return bindings.childrenNames.has(value.name);
28804
+ return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "children" && isPropsObjectExpression(value.object, bindings);
28805
+ };
28806
+ const collectChildrenAliases = (functionNode, bindings) => {
28592
28807
  const { body } = functionNode;
28593
- if (!body) return null;
28594
- if (!isNodeOfType(body, "BlockStatement")) return isNodeOfType(body, "JSXElement") ? resolveJsxElementName(body.openingElement) : null;
28595
- for (const statement of body.body) {
28596
- if (!isNodeOfType(statement, "ReturnStatement")) continue;
28597
- const argument = statement.argument;
28598
- if (argument && isNodeOfType(argument, "JSXElement")) return resolveJsxElementName(argument.openingElement);
28808
+ if (!body || !isNodeOfType(body, "BlockStatement")) return;
28809
+ for (let pass = 0; pass < MAX_CHILDREN_ALIAS_PASSES; pass += 1) {
28810
+ const sizeBeforePass = bindings.childrenNames.size;
28811
+ walkAst(body, (node) => {
28812
+ if (isFunctionNode(node)) return false;
28813
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return void 0;
28814
+ if (isNodeOfType(node.id, "Identifier")) {
28815
+ if (isChildrenValueExpression(node.init, bindings)) bindings.childrenNames.add(node.id.name);
28816
+ return;
28817
+ }
28818
+ if (isNodeOfType(node.id, "ObjectPattern") && isPropsObjectExpression(node.init, bindings)) for (const property of node.id.properties ?? []) {
28819
+ const localName = resolveChildrenPropertyLocalName(property);
28820
+ if (localName) bindings.childrenNames.add(localName);
28821
+ }
28822
+ });
28823
+ if (bindings.childrenNames.size === sizeBeforePass) break;
28824
+ }
28825
+ };
28826
+ const collectJsxRootsFromExpression = (expression, roots) => {
28827
+ const value = stripParenExpression(expression);
28828
+ if (isNodeOfType(value, "JSXElement") || isNodeOfType(value, "JSXFragment")) {
28829
+ roots.push(value);
28830
+ return;
28831
+ }
28832
+ if (isNodeOfType(value, "ConditionalExpression")) {
28833
+ if (value.consequent) collectJsxRootsFromExpression(value.consequent, roots);
28834
+ if (value.alternate) collectJsxRootsFromExpression(value.alternate, roots);
28835
+ return;
28836
+ }
28837
+ if (isNodeOfType(value, "LogicalExpression")) {
28838
+ if (value.left) collectJsxRootsFromExpression(value.left, roots);
28839
+ if (value.right) collectJsxRootsFromExpression(value.right, roots);
28840
+ }
28841
+ };
28842
+ const collectReturnedJsxRoots = (functionNode) => {
28843
+ const roots = [];
28844
+ const { body } = functionNode;
28845
+ if (!body) return roots;
28846
+ if (!isNodeOfType(body, "BlockStatement")) {
28847
+ collectJsxRootsFromExpression(body, roots);
28848
+ return roots;
28849
+ }
28850
+ walkAst(body, (node) => {
28851
+ if (isFunctionNode(node) && node !== functionNode) return false;
28852
+ if (isNodeOfType(node, "ReturnStatement") && node.argument) {
28853
+ collectJsxRootsFromExpression(node.argument, roots);
28854
+ return false;
28855
+ }
28856
+ });
28857
+ return roots;
28858
+ };
28859
+ const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
28860
+ const isChildrenForwardingAttribute = (attribute, bindings) => {
28861
+ if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
28862
+ return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
28863
+ };
28864
+ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElement) => {
28865
+ let didForwardIntoText = false;
28866
+ walkAst(jsxRoot, (node) => {
28867
+ if (didForwardIntoText || isFunctionNode(node)) return false;
28868
+ if (!isNodeOfType(node, "JSXElement")) return void 0;
28869
+ const elementName = resolveJsxElementName(node.openingElement);
28870
+ if (!elementName || !isTextHandlingElement(elementName)) return;
28871
+ didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
28872
+ });
28873
+ return didForwardIntoText;
28874
+ };
28875
+ const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
28876
+ const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
28877
+ let didRenderOutsideText = false;
28878
+ walkAst(jsxRoot, (node) => {
28879
+ if (didRenderOutsideText || isFunctionNode(node)) return false;
28880
+ if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
28881
+ if (isNodeOfType(node, "JSXElement")) {
28882
+ const elementName = resolveJsxElementName(node.openingElement);
28883
+ if (elementName && isTextHandlingElement(elementName)) return false;
28884
+ if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
28885
+ didRenderOutsideText = true;
28886
+ return;
28887
+ }
28888
+ }
28889
+ didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
28890
+ });
28891
+ return didRenderOutsideText;
28892
+ };
28893
+ const resolveStyledFactoryBaseName = (definitionNode) => {
28894
+ let current = stripParenExpression(definitionNode);
28895
+ while (current) {
28896
+ if (isNodeOfType(current, "TaggedTemplateExpression")) {
28897
+ current = stripParenExpression(current.tag);
28898
+ continue;
28899
+ }
28900
+ if (isNodeOfType(current, "CallExpression")) {
28901
+ const callee = stripParenExpression(current.callee);
28902
+ if (isNodeOfType(callee, "Identifier") && callee.name === "styled") {
28903
+ const baseArgument = current.arguments?.[0];
28904
+ if (!baseArgument) return null;
28905
+ const base = stripParenExpression(baseArgument);
28906
+ return isNodeOfType(base, "Identifier") ? base.name : null;
28907
+ }
28908
+ current = callee;
28909
+ continue;
28910
+ }
28911
+ if (isNodeOfType(current, "MemberExpression")) {
28912
+ if (isNodeOfType(current.object, "Identifier") && current.object.name === "styled" && isNodeOfType(current.property, "Identifier")) return current.property.name;
28913
+ current = stripParenExpression(current.object);
28914
+ continue;
28915
+ }
28916
+ return null;
28917
+ }
28918
+ return null;
28919
+ };
28920
+ const resolveClassRenderFunction = (classNode) => {
28921
+ if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
28922
+ for (const member of classNode.body?.body ?? []) {
28923
+ if (!isNodeOfType(member, "MethodDefinition")) continue;
28924
+ if (!isNodeOfType(member.key, "Identifier") || member.key.name !== "render") continue;
28925
+ return member.value && isFunctionNode(member.value) ? member.value : null;
28599
28926
  }
28600
28927
  return null;
28601
28928
  };
28602
- const recordWrapperFromDeclaration = (componentName, functionNode, isTextHandlingRoot, wrappers) => {
28929
+ const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
28603
28930
  if (!componentName || !isReactComponentName(componentName)) return;
28604
- if (!functionNode || !isFunctionNode(functionNode)) return;
28605
- const rootName = resolveReturnedRootElementName(functionNode);
28606
- if (rootName && isTextHandlingRoot(rootName)) wrappers.add(componentName);
28931
+ if (wrappers.has(componentName)) return;
28932
+ if (!definitionNode) return;
28933
+ const unwrapped = unwrapComponentDefinition(definitionNode);
28934
+ const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
28935
+ if (styledBaseName && isTextHandlingElement(styledBaseName)) {
28936
+ wrappers.add(componentName);
28937
+ return;
28938
+ }
28939
+ const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
28940
+ if (!functionNode) return;
28941
+ const bindings = resolveParamChildrenBindings(functionNode);
28942
+ collectChildrenAliases(functionNode, bindings);
28943
+ const jsxRoots = collectReturnedJsxRoots(functionNode);
28944
+ if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
28945
+ for (const jsxRoot of jsxRoots) {
28946
+ if (isNodeOfType(jsxRoot, "JSXElement")) {
28947
+ const rootName = resolveJsxElementName(jsxRoot.openingElement);
28948
+ if (rootName && isTextHandlingElement(rootName)) {
28949
+ wrappers.add(componentName);
28950
+ return;
28951
+ }
28952
+ }
28953
+ if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
28954
+ wrappers.add(componentName);
28955
+ return;
28956
+ }
28957
+ }
28607
28958
  };
28959
+ const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
28608
28960
  const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
28609
28961
  const wrappers = /* @__PURE__ */ new Set();
28610
- walkAst(programNode, (node) => {
28611
- if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingRoot, wrappers);
28612
- else if (isNodeOfType(node, "FunctionDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingRoot, wrappers);
28613
- });
28962
+ const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
28963
+ for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
28964
+ const sizeBeforePass = wrappers.size;
28965
+ walkAst(programNode, (node) => {
28966
+ if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
28967
+ else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
28968
+ });
28969
+ if (wrappers.size === sizeBeforePass) break;
28970
+ }
28614
28971
  return wrappers;
28615
28972
  };
28616
28973
  //#endregion
@@ -28678,6 +29035,7 @@ const rnNoRawText = defineRule({
28678
29035
  title: "Raw text outside a Text component",
28679
29036
  requires: ["react-native"],
28680
29037
  severity: "error",
29038
+ tags: ["test-noise"],
28681
29039
  recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
28682
29040
  create: (context) => {
28683
29041
  let isDomComponentFile = false;
@@ -28923,136 +29281,6 @@ const rnPreferContentInsetAdjustment = defineRetiredRule({
28923
29281
  recommendation: "Retired: SafeAreaView wrappers are valid; prefer native content inset adjustment only when manual inset plumbing causes scroll jumps or duplicated safe-area offsets."
28924
29282
  });
28925
29283
  //#endregion
28926
- //#region src/react-native-dependency-names.ts
28927
- const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
28928
- "expo",
28929
- "expo-router",
28930
- "@expo/cli",
28931
- "@expo/metro-config",
28932
- "@expo/metro-runtime"
28933
- ]);
28934
- const REACT_NATIVE_DEPENDENCY_NAMES = new Set([
28935
- "react-native",
28936
- "react-native-tvos",
28937
- ...EXPO_MANAGED_DEPENDENCY_NAMES,
28938
- "react-native-windows",
28939
- "react-native-macos"
28940
- ]);
28941
- const REACT_NATIVE_DEPENDENCY_PREFIXES = ["@react-native/", "@react-native-"];
28942
- const isExpoManagedDependencyName = (dependencyName) => EXPO_MANAGED_DEPENDENCY_NAMES.has(dependencyName);
28943
- const isReactNativeDependencyName = (dependencyName) => {
28944
- if (REACT_NATIVE_DEPENDENCY_NAMES.has(dependencyName)) return true;
28945
- for (const prefix of REACT_NATIVE_DEPENDENCY_PREFIXES) if (dependencyName.startsWith(prefix)) return true;
28946
- return false;
28947
- };
28948
- //#endregion
28949
- //#region src/plugin/utils/classify-package-platform.ts
28950
- const WEB_FRAMEWORK_DEPENDENCY_NAMES = new Set([
28951
- "next",
28952
- "vite",
28953
- "react-scripts",
28954
- "gatsby",
28955
- "@remix-run/react",
28956
- "@remix-run/node",
28957
- "@docusaurus/core",
28958
- "@docusaurus/preset-classic",
28959
- "@storybook/react",
28960
- "@storybook/react-vite",
28961
- "@storybook/react-webpack5",
28962
- "@storybook/nextjs",
28963
- "@storybook/web-components",
28964
- "storybook",
28965
- "react-dom",
28966
- "@vitejs/plugin-react",
28967
- "@vitejs/plugin-react-swc"
28968
- ]);
28969
- const cachedPlatformByPackageDirectory = /* @__PURE__ */ new Map();
28970
- const cachedPackageDirectoryByFilename = /* @__PURE__ */ new Map();
28971
- const findNearestPackageDirectory = (filename) => {
28972
- if (!filename) return null;
28973
- const fromCache = cachedPackageDirectoryByFilename.get(filename);
28974
- if (fromCache !== void 0) return fromCache;
28975
- let currentDirectory = path.dirname(filename);
28976
- while (true) {
28977
- const candidatePackageJsonPath = path.join(currentDirectory, "package.json");
28978
- let hasPackageJson = false;
28979
- try {
28980
- hasPackageJson = fs.statSync(candidatePackageJsonPath).isFile();
28981
- } catch {
28982
- hasPackageJson = false;
28983
- }
28984
- if (hasPackageJson) {
28985
- cachedPackageDirectoryByFilename.set(filename, currentDirectory);
28986
- return currentDirectory;
28987
- }
28988
- const parentDirectory = path.dirname(currentDirectory);
28989
- if (parentDirectory === currentDirectory) {
28990
- cachedPackageDirectoryByFilename.set(filename, null);
28991
- return null;
28992
- }
28993
- currentDirectory = parentDirectory;
28994
- }
28995
- };
28996
- const readPackageJsonSafe = (packageJsonPath) => {
28997
- let rawContents;
28998
- try {
28999
- rawContents = fs.readFileSync(packageJsonPath, "utf-8");
29000
- } catch {
29001
- return null;
29002
- }
29003
- try {
29004
- const parsed = JSON.parse(rawContents);
29005
- if (typeof parsed === "object" && parsed !== null) return parsed;
29006
- return null;
29007
- } catch {
29008
- return null;
29009
- }
29010
- };
29011
- const DEPENDENCY_SECTION_NAMES = [
29012
- "dependencies",
29013
- "devDependencies",
29014
- "peerDependencies",
29015
- "optionalDependencies"
29016
- ];
29017
- const iterateDependencyNames = function* (packageJson) {
29018
- for (const sectionName of DEPENDENCY_SECTION_NAMES) {
29019
- const section = packageJson[sectionName];
29020
- if (!section) continue;
29021
- for (const dependencyName of Object.keys(section)) yield dependencyName;
29022
- }
29023
- };
29024
- const isReactNativeAware = (packageJson) => {
29025
- if (typeof packageJson["react-native"] === "string") return true;
29026
- for (const dependencyName of iterateDependencyNames(packageJson)) if (isReactNativeDependencyName(dependencyName)) return true;
29027
- return false;
29028
- };
29029
- const isExpoManaged = (packageJson) => {
29030
- for (const dependencyName of iterateDependencyNames(packageJson)) if (isExpoManagedDependencyName(dependencyName)) return true;
29031
- return false;
29032
- };
29033
- const isWebFrameworkOnly = (packageJson) => {
29034
- for (const dependencyName of iterateDependencyNames(packageJson)) if (WEB_FRAMEWORK_DEPENDENCY_NAMES.has(dependencyName)) return true;
29035
- return false;
29036
- };
29037
- const classifyPackagePlatform = (filename) => {
29038
- const packageDirectory = findNearestPackageDirectory(filename);
29039
- if (!packageDirectory) return "unknown";
29040
- const cached = cachedPlatformByPackageDirectory.get(packageDirectory);
29041
- if (cached !== void 0) return cached;
29042
- const packageJson = readPackageJsonSafe(path.join(packageDirectory, "package.json"));
29043
- if (!packageJson) {
29044
- cachedPlatformByPackageDirectory.set(packageDirectory, "unknown");
29045
- return "unknown";
29046
- }
29047
- let result;
29048
- if (isExpoManaged(packageJson)) result = "expo";
29049
- else if (isReactNativeAware(packageJson)) result = "react-native";
29050
- else if (isWebFrameworkOnly(packageJson)) result = "web";
29051
- else result = "unknown";
29052
- cachedPlatformByPackageDirectory.set(packageDirectory, result);
29053
- return result;
29054
- };
29055
- //#endregion
29056
29284
  //#region src/plugin/utils/is-expo-managed-file.ts
29057
29285
  const isExpoManagedFileActive = (context) => {
29058
29286
  const rawFilename = context.filename;
@@ -39088,24 +39316,6 @@ const reactDoctorRules = [
39088
39316
  ];
39089
39317
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
39090
39318
  //#endregion
39091
- //#region src/plugin/utils/is-react-native-file.ts
39092
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
39093
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
39094
- const isReactNativeFileActive = (context) => {
39095
- const rawFilename = context.filename;
39096
- if (!rawFilename) return true;
39097
- const filename = normalizeFilename$1(rawFilename);
39098
- if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
39099
- if (WEB_FILE_EXTENSION_PATTERN.test(filename)) return false;
39100
- const packagePlatform = classifyPackagePlatform(filename);
39101
- if (packagePlatform === "web") return false;
39102
- if (packagePlatform === "expo" || packagePlatform === "react-native") return true;
39103
- const framework = getReactDoctorStringSetting(context.settings, "framework");
39104
- if (framework === "react-native" || framework === "expo") return true;
39105
- if (framework === "nextjs" || framework === "vite" || framework === "cra" || framework === "remix" || framework === "gatsby" || framework === "tanstack-start") return false;
39106
- return true;
39107
- };
39108
- //#endregion
39109
39319
  //#region src/plugin/utils/wrap-react-native-rule.ts
39110
39320
  const EMPTY_VISITORS = {};
39111
39321
  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.f63ed9d",
3
+ "version": "0.5.1-dev.fee3fc4",
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",