oxlint-plugin-react-doctor 0.5.1-dev.038aaf7 → 0.5.1-dev.19d99ee
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.
- package/dist/index.js +223 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28588,29 +28588,236 @@ const isInsidePlatformOsWebBranch = (node) => {
|
|
|
28588
28588
|
//#endregion
|
|
28589
28589
|
//#region src/plugin/rules/react-native/utils/collect-text-wrapper-components.ts
|
|
28590
28590
|
const isFunctionNode = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
28591
|
-
const
|
|
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) => {
|
|
28592
28657
|
const { body } = functionNode;
|
|
28593
|
-
if (!body) return
|
|
28594
|
-
|
|
28595
|
-
|
|
28596
|
-
|
|
28597
|
-
|
|
28598
|
-
|
|
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;
|
|
28599
28767
|
}
|
|
28600
28768
|
return null;
|
|
28601
28769
|
};
|
|
28602
|
-
const
|
|
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;
|
|
28776
|
+
}
|
|
28777
|
+
return null;
|
|
28778
|
+
};
|
|
28779
|
+
const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandlingElement, wrappers) => {
|
|
28603
28780
|
if (!componentName || !isReactComponentName(componentName)) return;
|
|
28604
|
-
if (
|
|
28605
|
-
|
|
28606
|
-
|
|
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
|
+
}
|
|
28607
28808
|
};
|
|
28809
|
+
const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
|
|
28608
28810
|
const collectTextWrapperComponents = (programNode, isTextHandlingRoot) => {
|
|
28609
28811
|
const wrappers = /* @__PURE__ */ new Set();
|
|
28610
|
-
|
|
28611
|
-
|
|
28612
|
-
|
|
28613
|
-
|
|
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
|
+
}
|
|
28614
28821
|
return wrappers;
|
|
28615
28822
|
};
|
|
28616
28823
|
//#endregion
|
|
@@ -28678,6 +28885,7 @@ const rnNoRawText = defineRule({
|
|
|
28678
28885
|
title: "Raw text outside a Text component",
|
|
28679
28886
|
requires: ["react-native"],
|
|
28680
28887
|
severity: "error",
|
|
28888
|
+
tags: ["test-noise"],
|
|
28681
28889
|
recommendation: "Text outside a `<Text>` component crashes on React Native. Wrap it like `<Text>{value}</Text>`.",
|
|
28682
28890
|
create: (context) => {
|
|
28683
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.
|
|
3
|
+
"version": "0.5.1-dev.19d99ee",
|
|
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",
|