oxlint-plugin-react-doctor 0.5.1-dev.c4f0e60 → 0.5.1-dev.f52bd07

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 +300 -79
  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([
@@ -12543,6 +12552,64 @@ const nextjsInlineScriptMissingId = defineRule({
12543
12552
  } })
12544
12553
  });
12545
12554
  //#endregion
12555
+ //#region src/plugin/utils/parse-export-specifiers.ts
12556
+ const getSpecifierName = (rawName) => rawName.replace(/^type\s+/, "").trim();
12557
+ const parseExportSpecifiers = (specifiersText, declarationIsTypeOnly) => specifiersText.split(",").map((specifierText) => specifierText.trim()).filter(Boolean).map((specifierText) => {
12558
+ const isTypeOnly = declarationIsTypeOnly || specifierText.startsWith("type ");
12559
+ const [rawLocalName, rawExportedName] = specifierText.split(/\s+as\s+/);
12560
+ const localName = getSpecifierName(rawLocalName ?? "");
12561
+ return {
12562
+ localName,
12563
+ exportedName: getSpecifierName(rawExportedName ?? localName),
12564
+ isTypeOnly
12565
+ };
12566
+ });
12567
+ //#endregion
12568
+ //#region src/plugin/utils/strip-js-comments.ts
12569
+ const BLOCK_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g;
12570
+ const LINE_COMMENT_PATTERN = /^\s*\/\/.*$/gm;
12571
+ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN, "").replace(LINE_COMMENT_PATTERN, "");
12572
+ //#endregion
12573
+ //#region src/plugin/utils/does-module-export-name.ts
12574
+ const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
12575
+ 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;
12576
+ const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
12577
+ const doesSourceTextExportName = (sourceText, exportedName) => {
12578
+ const strippedSource = stripJsComments(sourceText);
12579
+ if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
12580
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
12581
+ 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;
12582
+ return false;
12583
+ };
12584
+ const doesModuleExportName = (filePath, exportedName) => {
12585
+ try {
12586
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
12587
+ } catch {
12588
+ return false;
12589
+ }
12590
+ };
12591
+ //#endregion
12592
+ //#region src/plugin/utils/has-ancestor-layout-matching.ts
12593
+ const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
12594
+ const normalizedPage = pageFilename.replaceAll("\\", "/");
12595
+ let currentDirectory = path.dirname(normalizedPage);
12596
+ for (let level = 0; level < 30; level++) {
12597
+ for (const layoutFileName of LAYOUT_FILE_NAMES) {
12598
+ const layoutPath = path.join(currentDirectory, layoutFileName);
12599
+ if (layoutPath.replaceAll("\\", "/") === normalizedPage) continue;
12600
+ if (matchesLayout(layoutPath)) return true;
12601
+ }
12602
+ const parentDirectory = path.dirname(currentDirectory);
12603
+ if (path.basename(currentDirectory) === "app" && path.basename(parentDirectory) !== "app") break;
12604
+ if (parentDirectory === currentDirectory) break;
12605
+ currentDirectory = parentDirectory;
12606
+ }
12607
+ return false;
12608
+ };
12609
+ //#endregion
12610
+ //#region src/plugin/utils/find-ancestor-metadata-layout.ts
12611
+ const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
12612
+ //#endregion
12546
12613
  //#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
12547
12614
  const nextjsMissingMetadata = defineRule({
12548
12615
  id: "nextjs-missing-metadata",
@@ -12555,13 +12622,15 @@ const nextjsMissingMetadata = defineRule({
12555
12622
  const filename = normalizeFilename$1(context.filename ?? "");
12556
12623
  if (!PAGE_FILE_PATTERN.test(filename)) return;
12557
12624
  if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
12558
- if (!programNode.body?.some((statement) => {
12625
+ if (programNode.body?.some((statement) => {
12559
12626
  if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
12560
12627
  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"));
12628
+ if (isNodeOfType(declaration, "VariableDeclaration")) return declaration.declarations?.some((declarator) => isNodeOfType(declarator.id, "Identifier") && METADATA_EXPORT_NAMES.includes(declarator.id.name));
12562
12629
  if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
12563
- return false;
12564
- })) context.report({
12630
+ return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
12631
+ })) return;
12632
+ if (hasAncestorMetadataLayout(context.filename ?? "")) return;
12633
+ context.report({
12565
12634
  node: programNode,
12566
12635
  message: "This page has no metadata, so search engines and social previews get no title or description."
12567
12636
  });
@@ -13452,43 +13521,6 @@ const parseSourceFile = (absoluteFilePath) => {
13452
13521
  return parsedProgram;
13453
13522
  };
13454
13523
  //#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
13524
  //#region src/plugin/utils/is-barrel-index-module.ts
13493
13525
  const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
13494
13526
  const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
@@ -13972,29 +14004,10 @@ const astMentionsSuspense = (programNode) => {
13972
14004
  };
13973
14005
  //#endregion
13974
14006
  //#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
- };
14007
+ const hasAncestorSuspenseLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => {
14008
+ const programRoot = parseSourceFile(layoutPath);
14009
+ return Boolean(programRoot && astMentionsSuspense(programRoot));
14010
+ });
13998
14011
  //#endregion
13999
14012
  //#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
14000
14013
  const astContainsUseSearchParams = (root) => {
@@ -28575,29 +28588,236 @@ const isInsidePlatformOsWebBranch = (node) => {
28575
28588
  //#endregion
28576
28589
  //#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
28577
28590
  const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
28578
- const resolveReturnedRootElementName = (functionNode) => {
28591
+ const COMPONENT_WRAPPER_CALLEE_NAMES = new Set(["memo", "forwardRef"]);
28592
+ const resolveCalleeName = (callee) => {
28593
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
28594
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
28595
+ return null;
28596
+ };
28597
+ const unwrapComponentDefinition = (node) => {
28598
+ let current = stripParenExpression(node);
28599
+ while (isNodeOfType(current, "CallExpression")) {
28600
+ const calleeName = resolveCalleeName(current.callee);
28601
+ const firstArgument = current.arguments?.[0];
28602
+ if (!calleeName || !COMPONENT_WRAPPER_CALLEE_NAMES.has(calleeName) || !firstArgument) break;
28603
+ current = stripParenExpression(firstArgument);
28604
+ }
28605
+ return current;
28606
+ };
28607
+ const resolveChildrenPropertyLocalName = (property) => {
28608
+ if (!isNodeOfType(property, "Property")) return null;
28609
+ if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "children") return null;
28610
+ const value = property.value;
28611
+ if (isNodeOfType(value, "Identifier")) return value.name;
28612
+ if (isNodeOfType(value, "AssignmentPattern") && isNodeOfType(value.left, "Identifier")) return value.left.name;
28613
+ return null;
28614
+ };
28615
+ const resolveParamChildrenBindings = (functionNode) => {
28616
+ const bindings = {
28617
+ childrenNames: /* @__PURE__ */ new Set(),
28618
+ propsObjectNames: /* @__PURE__ */ new Set()
28619
+ };
28620
+ const firstParam = functionNode.params?.[0];
28621
+ if (!firstParam) return bindings;
28622
+ if (isNodeOfType(firstParam, "Identifier")) {
28623
+ bindings.propsObjectNames.add(firstParam.name);
28624
+ return bindings;
28625
+ }
28626
+ if (!isNodeOfType(firstParam, "ObjectPattern")) return bindings;
28627
+ let didDestructureChildren = false;
28628
+ let restName = null;
28629
+ for (const property of firstParam.properties ?? []) {
28630
+ if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) {
28631
+ restName = property.argument.name;
28632
+ continue;
28633
+ }
28634
+ const localName = resolveChildrenPropertyLocalName(property);
28635
+ if (localName) {
28636
+ didDestructureChildren = true;
28637
+ bindings.childrenNames.add(localName);
28638
+ }
28639
+ }
28640
+ if (restName && !didDestructureChildren) bindings.propsObjectNames.add(restName);
28641
+ return bindings;
28642
+ };
28643
+ const MAX_CHILDREN_ALIAS_PASSES = 3;
28644
+ const isPropsObjectExpression = (expression, bindings) => {
28645
+ if (!expression) return false;
28646
+ const value = stripParenExpression(expression);
28647
+ if (isNodeOfType(value, "Identifier")) return bindings.propsObjectNames.has(value.name);
28648
+ return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.object, "ThisExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "props";
28649
+ };
28650
+ const isChildrenValueExpression = (expression, bindings) => {
28651
+ if (!expression) return false;
28652
+ const value = stripParenExpression(expression);
28653
+ if (isNodeOfType(value, "Identifier")) return bindings.childrenNames.has(value.name);
28654
+ return isNodeOfType(value, "MemberExpression") && isNodeOfType(value.property, "Identifier") && value.property.name === "children" && isPropsObjectExpression(value.object, bindings);
28655
+ };
28656
+ const collectChildrenAliases = (functionNode, bindings) => {
28579
28657
  const { body } = functionNode;
28580
- if (!body) return null;
28581
- if (!isNodeOfType(body, "BlockStatement")) return isNodeOfType(body, "JSXElement") ? resolveJsxElementName(body.openingElement) : null;
28582
- for (const statement of body.body) {
28583
- if (!isNodeOfType(statement, "ReturnStatement")) continue;
28584
- const argument = statement.argument;
28585
- if (argument && isNodeOfType(argument, "JSXElement")) return resolveJsxElementName(argument.openingElement);
28658
+ if (!body || !isNodeOfType(body, "BlockStatement")) return;
28659
+ for (let pass = 0; pass < MAX_CHILDREN_ALIAS_PASSES; pass += 1) {
28660
+ const sizeBeforePass = bindings.childrenNames.size;
28661
+ walkAst(body, (node) => {
28662
+ if (isFunctionNode(node)) return false;
28663
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return void 0;
28664
+ if (isNodeOfType(node.id, "Identifier")) {
28665
+ if (isChildrenValueExpression(node.init, bindings)) bindings.childrenNames.add(node.id.name);
28666
+ return;
28667
+ }
28668
+ if (isNodeOfType(node.id, "ObjectPattern") && isPropsObjectExpression(node.init, bindings)) for (const property of node.id.properties ?? []) {
28669
+ const localName = resolveChildrenPropertyLocalName(property);
28670
+ if (localName) bindings.childrenNames.add(localName);
28671
+ }
28672
+ });
28673
+ if (bindings.childrenNames.size === sizeBeforePass) break;
28674
+ }
28675
+ };
28676
+ const collectJsxRootsFromExpression = (expression, roots) => {
28677
+ const value = stripParenExpression(expression);
28678
+ if (isNodeOfType(value, "JSXElement") || isNodeOfType(value, "JSXFragment")) {
28679
+ roots.push(value);
28680
+ return;
28681
+ }
28682
+ if (isNodeOfType(value, "ConditionalExpression")) {
28683
+ if (value.consequent) collectJsxRootsFromExpression(value.consequent, roots);
28684
+ if (value.alternate) collectJsxRootsFromExpression(value.alternate, roots);
28685
+ return;
28686
+ }
28687
+ if (isNodeOfType(value, "LogicalExpression")) {
28688
+ if (value.left) collectJsxRootsFromExpression(value.left, roots);
28689
+ if (value.right) collectJsxRootsFromExpression(value.right, roots);
28690
+ }
28691
+ };
28692
+ const collectReturnedJsxRoots = (functionNode) => {
28693
+ const roots = [];
28694
+ const { body } = functionNode;
28695
+ if (!body) return roots;
28696
+ if (!isNodeOfType(body, "BlockStatement")) {
28697
+ collectJsxRootsFromExpression(body, roots);
28698
+ return roots;
28699
+ }
28700
+ walkAst(body, (node) => {
28701
+ if (isFunctionNode(node) && node !== functionNode) return false;
28702
+ if (isNodeOfType(node, "ReturnStatement") && node.argument) {
28703
+ collectJsxRootsFromExpression(node.argument, roots);
28704
+ return false;
28705
+ }
28706
+ });
28707
+ return roots;
28708
+ };
28709
+ const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
28710
+ const isChildrenForwardingAttribute = (attribute, bindings) => {
28711
+ if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
28712
+ return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
28713
+ };
28714
+ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElement) => {
28715
+ let didForwardIntoText = false;
28716
+ walkAst(jsxRoot, (node) => {
28717
+ if (didForwardIntoText || isFunctionNode(node)) return false;
28718
+ if (!isNodeOfType(node, "JSXElement")) return void 0;
28719
+ const elementName = resolveJsxElementName(node.openingElement);
28720
+ if (!elementName || !isTextHandlingElement(elementName)) return;
28721
+ didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
28722
+ });
28723
+ return didForwardIntoText;
28724
+ };
28725
+ const isMeaningfulJsxChild = (child) => !isNodeOfType(child, "JSXText") || Boolean(child.value?.trim());
28726
+ const jsxRootRendersChildrenOutsideText = (jsxRoot, bindings, isTextHandlingElement) => {
28727
+ let didRenderOutsideText = false;
28728
+ walkAst(jsxRoot, (node) => {
28729
+ if (didRenderOutsideText || isFunctionNode(node)) return false;
28730
+ if (!isNodeOfType(node, "JSXElement") && !isNodeOfType(node, "JSXFragment")) return;
28731
+ if (isNodeOfType(node, "JSXElement")) {
28732
+ const elementName = resolveJsxElementName(node.openingElement);
28733
+ if (elementName && isTextHandlingElement(elementName)) return false;
28734
+ if (!(node.children ?? []).some(isMeaningfulJsxChild) && (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings))) {
28735
+ didRenderOutsideText = true;
28736
+ return;
28737
+ }
28738
+ }
28739
+ didRenderOutsideText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings));
28740
+ });
28741
+ return didRenderOutsideText;
28742
+ };
28743
+ const resolveStyledFactoryBaseName = (definitionNode) => {
28744
+ let current = stripParenExpression(definitionNode);
28745
+ while (current) {
28746
+ if (isNodeOfType(current, "TaggedTemplateExpression")) {
28747
+ current = stripParenExpression(current.tag);
28748
+ continue;
28749
+ }
28750
+ if (isNodeOfType(current, "CallExpression")) {
28751
+ const callee = stripParenExpression(current.callee);
28752
+ if (isNodeOfType(callee, "Identifier") && callee.name === "styled") {
28753
+ const baseArgument = current.arguments?.[0];
28754
+ if (!baseArgument) return null;
28755
+ const base = stripParenExpression(baseArgument);
28756
+ return isNodeOfType(base, "Identifier") ? base.name : null;
28757
+ }
28758
+ current = callee;
28759
+ continue;
28760
+ }
28761
+ if (isNodeOfType(current, "MemberExpression")) {
28762
+ if (isNodeOfType(current.object, "Identifier") && current.object.name === "styled" && isNodeOfType(current.property, "Identifier")) return current.property.name;
28763
+ current = stripParenExpression(current.object);
28764
+ continue;
28765
+ }
28766
+ return null;
28767
+ }
28768
+ return null;
28769
+ };
28770
+ const resolveClassRenderFunction = (classNode) => {
28771
+ if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
28772
+ for (const member of classNode.body?.body ?? []) {
28773
+ if (!isNodeOfType(member, "MethodDefinition")) continue;
28774
+ if (!isNodeOfType(member.key, "Identifier") || member.key.name !== "render") continue;
28775
+ return member.value && isFunctionNode(member.value) ? member.value : null;
28586
28776
  }
28587
28777
  return null;
28588
28778
  };
28589
- const recordWrapperFromDeclaration = (componentName, functionNode, isTextHandlingRoot, wrappers) => {
28779
+ const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
28590
28780
  if (!componentName || !isReactComponentName(componentName)) return;
28591
- if (!functionNode || !isFunctionNode(functionNode)) return;
28592
- const rootName = resolveReturnedRootElementName(functionNode);
28593
- if (rootName && isTextHandlingRoot(rootName)) wrappers.add(componentName);
28781
+ if (wrappers.has(componentName)) return;
28782
+ if (!definitionNode) return;
28783
+ const unwrapped = unwrapComponentDefinition(definitionNode);
28784
+ const styledBaseName = resolveStyledFactoryBaseName(unwrapped);
28785
+ if (styledBaseName && isTextHandlingElement(styledBaseName)) {
28786
+ wrappers.add(componentName);
28787
+ return;
28788
+ }
28789
+ const functionNode = resolveClassRenderFunction(unwrapped) ?? (isFunctionNode(unwrapped) ? unwrapped : null);
28790
+ if (!functionNode) return;
28791
+ const bindings = resolveParamChildrenBindings(functionNode);
28792
+ collectChildrenAliases(functionNode, bindings);
28793
+ const jsxRoots = collectReturnedJsxRoots(functionNode);
28794
+ if (jsxRoots.some((jsxRoot) => jsxRootRendersChildrenOutsideText(jsxRoot, bindings, isTextHandlingElement))) return;
28795
+ for (const jsxRoot of jsxRoots) {
28796
+ if (isNodeOfType(jsxRoot, "JSXElement")) {
28797
+ const rootName = resolveJsxElementName(jsxRoot.openingElement);
28798
+ if (rootName && isTextHandlingElement(rootName)) {
28799
+ wrappers.add(componentName);
28800
+ return;
28801
+ }
28802
+ }
28803
+ if (jsxRootForwardsChildrenIntoText(jsxRoot, bindings, isTextHandlingElement)) {
28804
+ wrappers.add(componentName);
28805
+ return;
28806
+ }
28807
+ }
28594
28808
  };
28809
+ const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
28595
28810
  const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
28596
28811
  const wrappers = /* @__PURE__ */ new Set();
28597
- walkAst(programNode, (node) => {
28598
- if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingRoot, wrappers);
28599
- else if (isNodeOfType(node, "FunctionDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingRoot, wrappers);
28600
- });
28812
+ const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
28813
+ for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
28814
+ const sizeBeforePass = wrappers.size;
28815
+ walkAst(programNode, (node) => {
28816
+ if (isNodeOfType(node, "VariableDeclarator")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node.init, isTextHandlingElement, wrappers);
28817
+ else if (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "ClassDeclaration")) recordWrapperFromDeclaration(node.id && isNodeOfType(node.id, "Identifier") ? node.id.name : null, node, isTextHandlingElement, wrappers);
28818
+ });
28819
+ if (wrappers.size === sizeBeforePass) break;
28820
+ }
28601
28821
  return wrappers;
28602
28822
  };
28603
28823
  //#endregion
@@ -28665,6 +28885,7 @@ const rnNoRawText = defineRule({
28665
28885
  title: "Raw text outside a Text component",
28666
28886
  requires: ["react-native"],
28667
28887
  severity: "error",
28888
+ tags: ["test-noise"],
28668
28889
  recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
28669
28890
  create: (context) => {
28670
28891
  let isDomComponentFile = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.1-dev.c4f0e60",
3
+ "version": "0.5.1-dev.f52bd07",
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",