diff --git a/index.js b/index.js
index a568194..bf51335 100644
--- a/index.js
+++ b/index.js
@@ -1,101 +1,71 @@
-const [DONE_WITH_TREE, DONE_WITH_SUBTREE] = [1, 2];
+const providerRegex = /Provider$/;
-function traverseTree(node, visitorKeys, callback) {
- const stack = [[node, [], null]];
-
- while (stack.length > 0) {
- try {
- const current = stack.shift();
- const [currentNode, ancestors] = current;
-
- callback(...current);
-
- for (const visitorKey of visitorKeys[currentNode.type] ?? []) {
- const child = currentNode[visitorKey];
-
- if (Array.isArray(child)) {
- for (const childItem of child) {
- stack.push([childItem, [currentNode, ...ancestors], visitorKey]);
- }
- } else if (Boolean(child)) {
- stack.push([child, [currentNode, ...ancestors], visitorKey]);
- }
- }
- } catch (code) {
- if (code === DONE_WITH_TREE) {
- break;
- } else if (code === DONE_WITH_SUBTREE) {
- continue;
- }
- }
- }
-}
-
-function getReturnStatement(node) {
- if (!Boolean(node)) {
- return;
- }
-
- if (node.type === 'ClassDeclaration') {
- // For class-based components, find the render function, then its return statement
- renderFunction = node.body?.body?.find(
- (statement) =>
- statement.type === 'MethodDefinition' &&
- statement.key.name === 'render',
- );
- return renderFunction.value?.body?.body?.find(
- (statement) => statement.type === 'ReturnStatement',
- );
+function handleJSX(context, name, jsx) {
+ if (
+ !providerRegex.test(name) &&
+ !jsx.openingElement.attributes.find(
+ (a) => a.name?.name === 'data-component',
+ )
+ ) {
+ context.report({
+ node: jsx,
+ message: `${name} is missing the data-component attribute for the top-level element.`,
+ fix(fixer) {
+ return fixer.insertTextAfterRange(
+ jsx.openingElement.typeParameters
+ ? jsx.openingElement.typeParameters.range
+ : jsx.openingElement.name.range,
+ ` data-component="${name}"`,
+ );
+ },
+ });
}
-
- if (node.type === 'ArrowFunctionExpression') return node.body;
- return node.type === 'VariableDeclaration'
- ? node.declarations?.[0]?.init?.body?.body?.find(
- (statement) => statement.type === 'ReturnStatement',
- ) ??
- node.declarations?.[0]?.init?.arguments?.[0]?.body ??
- node.declarations?.[0]?.init?.body
- : node.body?.body?.find(
- (statement) => statement.type === 'ReturnStatement',
- );
}
-function isForwardRef(node) {
- if (!Boolean(node)) {
- return;
+function handleBlockStatement(context, name, block) {
+ // Find the root return statement. Are there any other types of returns we need to handle?
+ const ret = block.body.find((c) => c.type === 'ReturnStatement');
+ if (ret && ret.argument.type === 'JSXElement') {
+ handleJSX(context, name, ret.argument);
}
-
- return node.type === 'VariableDeclaration'
- ? node.declarations?.[0]?.init?.callee?.property?.name === 'forwardRef'
- : node.callee?.name === 'forwardRef' ||
- node.callee?.property?.name === 'forwardRef';
}
-function isTreeDone(node, excludeComponentNames) {
- return (
- node.type === 'JSXElement' &&
- excludeComponentNames.every(
- (regex) =>
- !regex.test(
- node.openingElement.name.property
- ? node.openingElement.name.property.name
- : node.openingElement.name.name,
- ),
- ) &&
- !node.openingElement.attributes.find(
- (attributeNode) => attributeNode.name?.name === 'data-component',
- )
- );
+function isForwardRef(expression) {
+ const calleeName =
+ expression.callee.type === 'MemberExpression'
+ ? expression.callee.property.name
+ : expression.callee.name;
+ return calleeName === 'forwardRef' && expression.arguments.length == 1;
}
-function isSubtreeDone(node) {
- return (
- node.type === 'JSXFragment' ||
- (node.type === 'JSXElement' &&
- node.openingElement.attributes.find(
- (attributeNode) => attributeNode.name?.name === 'data-component',
- ))
- );
+function handleExpression(context, name, expression) {
+ switch (expression.type) {
+ case 'FunctionExpression':
+ handleBlockStatement(context, name, expression.body);
+ break;
+ case 'ArrowFunctionExpression':
+ switch (expression.body.type) {
+ case 'JSXElement':
+ handleJSX(context, name, expression.body);
+ break;
+ case 'BlockStatement':
+ handleBlockStatement(context, name, expression.body);
+ break;
+ }
+ break;
+ case 'ClassExpression':
+ expression.body.body.forEach((x) => {
+ if (x.type === 'MethodDefinition' && x.key.name === 'render') {
+ handleBlockStatement(context, name, x.value.body);
+ }
+ });
+ break;
+ case 'CallExpression':
+ if (isForwardRef(expression)) {
+ handleExpression(context, name, expression.arguments[0]);
+ }
+ break;
+ }
}
const rules = {
@@ -111,112 +81,53 @@ const rules = {
fixable: 'code',
},
create(context) {
- const { visitorKeys } = context.getSourceCode();
-
- const excludeComponentNames =
- context.options?.[0]?.excludeComponentNames?.map(
- (regex) => new RegExp(regex),
- ) ?? [/Provider$/];
-
return {
- Program(node) {
- const componentNodes = node.body
- .map((child) => {
- const declaration = child?.declaration ?? child;
- if (isForwardRef(declaration)) {
- // do something
- return declaration?.arguments?.[0] ?? declaration;
- }
- return declaration;
- })
- .filter(
- (child) =>
- child.type === 'VariableDeclaration' ||
- child.type === 'FunctionDeclaration' ||
- child.type === 'ClassDeclaration' ||
- child.type === 'FunctionExpression' ||
- child.type === 'ArrowFunctionExpression',
- )
- .filter((child) => {
- let flag = false;
-
- traverseTree(
- getReturnStatement(child),
- visitorKeys,
- (current) => {
- if (current.type === 'JSXElement') {
- flag = true;
-
- throw DONE_WITH_TREE;
- }
- },
- );
-
- return flag;
- })
- .filter((child) => {
- let flag = false;
-
- traverseTree(
- getReturnStatement(child),
- visitorKeys,
- (current) => {
- if (isSubtreeDone(current)) {
- throw DONE_WITH_SUBTREE;
- } else if (isTreeDone(current, excludeComponentNames)) {
- flag = true;
+ Program(root) {
+ root.body.forEach((node) => {
+ // We will need to save any non-exported declarations and handle them only if they get exported at the end
+ let exported = false;
+ if (
+ (node.type === 'ExportNamedDeclaration' ||
+ node.type === 'ExportDefaultDeclaration') &&
+ node.declaration
+ ) {
+ exported = true;
+ node = node.declaration;
+ }
- throw DONE_WITH_TREE;
+ switch (node.type) {
+ case 'VariableDeclaration':
+ node.declarations.forEach((variable) => {
+ handleExpression(context, variable.id.name, variable.init);
+ });
+ break;
+ case 'FunctionDeclaration':
+ handleBlockStatement(context, node.id.name, node.body);
+ break;
+ case 'ClassDeclaration':
+ node.body.body.forEach((x) => {
+ if (
+ x.type === 'MethodDefinition' &&
+ x.key.name === 'render'
+ ) {
+ handleBlockStatement(context, node.id.name, x.value.body);
+ return;
}
- },
- );
-
- return flag;
- });
-
- componentNodes.forEach((componentNode) => {
- const componentName =
- componentNode?.id?.name ??
- componentNode?.declarations?.map(
- (declaration) => declaration?.id?.name,
- );
-
- let fixNode = null;
-
- traverseTree(
- getReturnStatement(componentNode),
- visitorKeys,
- (current) => {
- if (isSubtreeDone(current)) {
- throw DONE_WITH_SUBTREE;
- } else if (isTreeDone(current, excludeComponentNames)) {
- fixNode = current.openingElement;
-
- throw DONE_WITH_TREE;
+ });
+ break;
+ case 'CallExpression':
+ if (
+ isForwardRef(node) &&
+ node.arguments[0].type === 'FunctionExpression' &&
+ node.arguments[0].id
+ ) {
+ handleExpression(
+ context,
+ node.arguments[0].id.name,
+ node.arguments[0],
+ );
}
- },
- );
-
- if (Boolean(componentName)) {
- context.report({
- node: fixNode,
- message: `${
- Array.isArray(componentName)
- ? componentName[0]
- : componentName
- } is missing the data-component attribute for the top-level element.`,
- fix: (fixer) =>
- fixer.insertTextAfterRange(
- Boolean(fixNode.typeParameters)
- ? fixNode.typeParameters.range
- : fixNode.name.range,
- ` data-component="${
- Array.isArray(componentName)
- ? componentName[0]
- : componentName
- }"`,
- ),
- });
+ break;
}
});
},
diff --git a/test.js b/test.js
index 5a554fe..5057079 100644
--- a/test.js
+++ b/test.js
@@ -7,11 +7,11 @@ const { join } = require('path');
//------------------------------------------------------------------------------
const singleComponent = `const temp = () => {
- ;
+ return ;
};`;
const singleComponentError = `const temp = () => {
- ;
+ return ;
};`;
const genericTest = `
@@ -241,6 +241,9 @@ export default forwardRef(
}
);`;
+const provider = /* tsx */ `
+export const MyProvider = () => ;
+`;
const tests = {
'data-component': {
// Require the actual rule definition
@@ -298,6 +301,9 @@ const tests = {
{
code: defaultForwardRef,
},
+ {
+ code: provider,
+ },
],
invalid: [
{