diff --git a/package.json b/package.json index 21b5298..02d962e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "postversion": "git push && git push --tags" }, "types": "dist/index.d.ts", + "peerDependencies": { + "eslint-plugin-react": "^7.23.2" + }, "devDependencies": { "@types/eslint": "^7.2.9", "@types/estree": "^0.0.47", diff --git a/src/eslint-plugin-react.d.ts b/src/eslint-plugin-react.d.ts new file mode 100644 index 0000000..1ae1e31 --- /dev/null +++ b/src/eslint-plugin-react.d.ts @@ -0,0 +1,3 @@ +declare module "eslint-plugin-react/lib/util/Components"; + +declare module "eslint-plugin-react/lib/util/ast"; diff --git a/src/index.ts b/src/index.ts index 175dd71..98a992f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,19 @@ module.exports = { "error", }, }, + settings: { + react: { + // Regex for Component Factory to use (defaults to "createReactClass") + createClass: "createReactClass", + // Pragma to use (defaults to "React") + pragma: "React", + // Fragment to use (may be a property of ), default to "Fragment" + fragment: "Fragment", + // React version. "detect" automatically picks the version you have installed. + // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. + version: "detect", + }, + }, }, rules: { "react-prefer-function-component": PreferFunctionComponent, diff --git a/src/prefer-function-component/index.ts b/src/prefer-function-component/index.ts index a29bdad..6f72d75 100644 --- a/src/prefer-function-component/index.ts +++ b/src/prefer-function-component/index.ts @@ -2,52 +2,24 @@ * @fileoverview Enforce components are written as function components */ +// TODO: improve typing +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ + import type { Rule } from "eslint"; +// Using eslint-plugin-react internals for upstream consideration +// https://github.com/yannickcr/eslint-plugin-react/issues/2860#issuecomment-819784530 +import Components from "eslint-plugin-react/lib/util/Components"; +import { + getComponentProperties, + getPropertyName, +} from "eslint-plugin-react/lib/util/ast"; -// TODO: -// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) -// https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/util/pragma.js -const pragma = "React"; -const createClass = "createReactClass"; export const COMPONENT_SHOULD_BE_FUNCTION = "componentShouldBeFunction"; export const ALLOW_COMPONENT_DID_CATCH = "allowComponentDidCatch"; const COMPONENT_DID_CATCH = "componentDidCatch"; // https://eslint.org/docs/developer-guide/working-with-rules const PROGRAM_EXIT = "Program:exit"; -// TODO: Type definitions -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Node = any; - -/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */ - -function getComponentProperties(node: Node): Node[] { - switch (node.type) { - case "ClassDeclaration": - case "ClassExpression": - return node.body.body; - case "ObjectExpression": - return node.properties; - default: - return []; - } -} - -function getPropertyNameNode(node: Node): Node | undefined { - if (node.key || ["MethodDefinition", "Property"].indexOf(node.type) !== -1) { - return node.key; - } - if (node.type === "MemberExpression") { - return node.property; - } - return undefined; -} - -function getPropertyName(node: Node): string { - const nameNode = getPropertyNameNode(node); - return nameNode ? nameNode.name : ""; -} - // https://eslint.org/docs/developer-guide/working-with-rules const rule: Rule.RuleModule = { meta: { @@ -78,32 +50,11 @@ const rule: Rule.RuleModule = { ], }, - create(context: Rule.RuleContext) { + create: Components.detect((context: any, components: any, utils: any) => { const allowComponentDidCatch = context.options[0]?.allowComponentDidCatch ?? true; - const sourceCode = context.getSourceCode(); - - function isES5Component(node: Node): boolean { - if (!node.parent) { - return false; - } - return new RegExp(`^(${pragma}\\.)?${createClass}$`).test( - sourceCode.getText(node.parent.callee) - ); - } - - function isES6Component(node: Node): boolean { - if (!node.superClass) { - return false; - } - - return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test( - sourceCode.getText(node.superClass) - ); - } - - function shouldPreferFunction(node: Node): boolean { + function shouldPreferFunction(node: any): boolean { if (!allowComponentDidCatch) { return true; } @@ -112,29 +63,32 @@ const rule: Rule.RuleModule = { return !properties.includes(COMPONENT_DID_CATCH); } - const components = new Set(); - - const detect = (guard: (node: Node) => boolean) => (node: Node) => { + const detect = (guard: (node: any) => boolean) => (node: any) => { if (guard(node) && shouldPreferFunction(node)) { - components.add(node); + components.set(node, { + [COMPONENT_SHOULD_BE_FUNCTION]: true, + }); } }; return { - ObjectExpression: detect(isES5Component), - ClassDeclaration: detect(isES6Component), - ClassExpression: detect(isES6Component), + ObjectExpression: detect(utils.isES5Component), + ClassDeclaration: detect(utils.isES6Component), + ClassExpression: detect(utils.isES6Component), [PROGRAM_EXIT]() { - components.forEach((node) => { - context.report({ - node, - messageId: COMPONENT_SHOULD_BE_FUNCTION, - }); + const list = components.list(); + Object.values(list).forEach((component: any) => { + if (component[COMPONENT_SHOULD_BE_FUNCTION]) { + context.report({ + node: component.node, + messageId: COMPONENT_SHOULD_BE_FUNCTION, + }); + } }); }, }; - }, + }), }; export default rule; diff --git a/src/prefer-function-component/test.ts b/src/prefer-function-component/test.ts index e20a50f..ac7ea0e 100644 --- a/src/prefer-function-component/test.ts +++ b/src/prefer-function-component/test.ts @@ -12,6 +12,11 @@ const ruleTester = new RuleTester({ jsx: true, }, }, + settings: { + react: { + version: "latest", + }, + }, }); ruleTester.run("prefer-function-component", rule, {