Skip to content

Commit

Permalink
migrate to eslint-plugin-react internals
Browse files Browse the repository at this point in the history
Set the stage for upstream consideration in eslint-plugin-react: jsx-eslint/eslint-plugin-react#2860 (comment)
  • Loading branch information
Tate committed Apr 16, 2021
1 parent 316cf7b commit 3175901
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 74 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/eslint-plugin-react.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "eslint-plugin-react/lib/util/Components";

declare module "eslint-plugin-react/lib/util/ast";
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pragma>), 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,
Expand Down
102 changes: 28 additions & 74 deletions src/prefer-function-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
}
Expand All @@ -112,29 +63,32 @@ const rule: Rule.RuleModule = {
return !properties.includes(COMPONENT_DID_CATCH);
}

const components = new Set<Node>();

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;
5 changes: 5 additions & 0 deletions src/prefer-function-component/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const ruleTester = new RuleTester({
jsx: true,
},
},
settings: {
react: {
version: "latest",
},
},
});

ruleTester.run("prefer-function-component", rule, {
Expand Down

0 comments on commit 3175901

Please sign in to comment.