Skip to content

Commit 38e5acb

Browse files
authored
v2.0.0 RC1 (#5)
1 parent 7ac1f3d commit 38e5acb

File tree

9 files changed

+1176
-1392
lines changed

9 files changed

+1176
-1392
lines changed

.github/workflows/publish.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
cache: "yarn"
1414
registry-url: "https://registry.npmjs.org"
1515
- run: yarn
16-
- run: yarn build
17-
- run: npm publish
16+
- run: yarn package:build
17+
- run: cd dist && npm publish
1818
env:
1919
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dist
22
coverage
3+
.prettierignore

CHANGELOG.md

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## v2.0.0
4+
5+
Support for `createClass` has been dropped. Usage of `createClass` will no longer be detected.
6+
7+
Now errors on any JSX usage within a `class`. Previously the below was not caught:
8+
9+
```jsx
10+
import Document from "next/document";
11+
12+
class MyDocument extends Document {
13+
render() {
14+
<>...</>;
15+
}
16+
}
17+
```
18+
319
## v1.0.0
420

521
No API changes. This library will now follow [semantic versioning](https://docs.npmjs.com/about-semantic-versioning).

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# eslint-plugin-react-prefer-function-component
22

3-
<blockquote>ESLint lint rule to enforce function components in React</blockquote>
3+
<blockquote>An [ESLint](https://github.com/eslint/eslint) plugin that prevents the use of React class components.</blockquote>
44

55
<br />
66

@@ -25,7 +25,7 @@
2525

2626
## What is this? 🧐
2727

28-
An [ESLint](https://github.com/eslint/eslint) plugin that prevents the use of React class components.
28+
An [ESLint](https://github.com/eslint/eslint) plugin that prevents the use of React class components. While this plugin specifically calls out React, it will work with Preact, Inferno, or other JSX libraries.
2929

3030
## Motivation
3131

@@ -48,11 +48,11 @@ This option is configurable.
4848

4949
> What about [eslint-plugin-react/prefer-stateless-function](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prefer-stateless-function.md)?
5050
51-
`eslint-plugin-react/prefer-stateless-function` allows class components that implement any class methods or properties. This rule is stricter and prevents the use of _any_ class components. See this [Stack Overflow question](https://stackoverflow.com/questions/63333796/how-to-use-react-with-function-component-and-hooks-only) for more context.
51+
`eslint-plugin-react/prefer-stateless-function` allows class components that implement any methods or properties other than `render`. This rule is stricter and prevents the use of _any_ class components. This [open issue](https://github.com/jsx-eslint/eslint-plugin-react/issues/2860) explains the limitations of `prefer-stateless-function` and the motivations for this plugin.
5252

5353
> Why didn't you contribute this rule to [https://github.com/yannickcr/eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react)?
5454
55-
I'm discussing this in an [open issue](https://github.com/yannickcr/eslint-plugin-react/issues/2860#issuecomment-819784530) and [pull request](https://github.com/yannickcr/eslint-plugin-react/pull/3040) on `eslint-plugin-react`. At this time, the maintainer is unconvinced that function component enforcement should be a lint rule.
55+
I'm discussing this in an [open issue](https://github.com/yannickcr/eslint-plugin-react/issues/2860#issuecomment-819784530) and [pull request](https://github.com/yannickcr/eslint-plugin-react/pull/3040) on `eslint-plugin-react`. At this time, the maintainer of `eslint-plugin-react` is unconvinced that function component enforcement should be a lint rule. If you would like to see this rule added to `eslint-plugin-react`, please join the discussion on the issue or pull request.
5656

5757
## Installation & Usage 📦
5858

package.json

+20-30
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
2-
"name": "eslint-plugin-react-prefer-function-component",
3-
"version": "1.0.0",
4-
"description": "ESLint lint rule to enforce function components in React",
2+
"name": "eslint-plugin-react-prefer-function-component-development",
3+
"description": "ESLint plugin that prevents the use of JSX class components",
54
"license": "MIT",
65
"author": "Tate <[email protected]>",
76
"homepage": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component#readme",
@@ -12,11 +11,6 @@
1211
"bugs": {
1312
"url": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/issues"
1413
},
15-
"main": "dist/index.js",
16-
"files": [
17-
"dist/index.d.ts",
18-
"dist/prefer-function-component/index.js"
19-
],
2014
"scripts": {
2115
"build": "yarn clean && yarn tsc",
2216
"build:watch": "yarn build --watch",
@@ -26,39 +20,35 @@
2620
"lint:fix:md": "prettier --write '*.md'",
2721
"lint:fix:package": "prettier-package-json --write package.json",
2822
"lint:fix:ts": "eslint --fix './src/**/*.ts{,x}'",
23+
"package:build": "yarn build && yarn package:prune && yarn package:copy:files",
24+
"package:copy:files": "cp ./LICENSE ./README.md dist/ && cp ./public.package.json dist/package.json",
25+
"package:prune": "find dist -name test.* -delete",
2926
"test": "yarn jest src/*",
3027
"test:ci": "yarn test --coverage",
3128
"typecheck": "yarn tsc --noEmit",
3229
"typecheck:watch": "yarn typecheck --watch"
3330
},
34-
"types": "dist/index.d.ts",
3531
"devDependencies": {
36-
"@babel/preset-env": "^7.14.1",
32+
"@babel/preset-env": "^7.17.10",
3733
"@babel/preset-react": "^7.13.13",
3834
"@babel/preset-typescript": "^7.13.0",
39-
"@types/eslint": "^8.4.0",
40-
"@types/estree": "^0.0.50",
41-
"@types/jest": "^27.4.0",
42-
"@types/node": "^17.0.10",
43-
"@typescript-eslint/eslint-plugin": "^5.10.0",
44-
"@typescript-eslint/parser": "^5.10.0",
35+
"@types/eslint": "^8.4.1",
36+
"@types/estree": "^0.0.51",
37+
"@types/jest": "^27.4.1",
38+
"@types/node": "^17.0.30",
39+
"@typescript-eslint/eslint-plugin": "^5.21.0",
40+
"@typescript-eslint/parser": "^5.21.0",
4541
"codecov": "^3.8.3",
46-
"eslint": "^8.7.0",
47-
"eslint-config-prettier": "^8.1.0",
48-
"eslint-plugin-react": "^7.28.0",
49-
"eslint-plugin-react-hooks": "^4.3.0",
42+
"eslint": "^8.14.0",
43+
"eslint-config-prettier": "^8.5.0",
44+
"eslint-plugin-react": "^7.29.4",
45+
"eslint-plugin-react-hooks": "^4.5.0",
5046
"husky": "^4.3.0",
51-
"jest": "^27.4.7",
52-
"prettier": "^2.5.1",
53-
"prettier-package-json": "^2.6.0",
54-
"typescript": "^4.5.5"
47+
"jest": "^28.0.3",
48+
"prettier": "^2.6.2",
49+
"prettier-package-json": "^2.6.3",
50+
"typescript": "^4.6.4"
5551
},
56-
"keywords": [
57-
"eslint react no class",
58-
"react function component",
59-
"react functional component",
60-
"react no class"
61-
],
6252
"husky": {
6353
"hooks": {
6454
"pre-commit": "yarn lint"

public.package.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "eslint-plugin-react-prefer-function-component",
3+
"version": "2.0.0-rc1",
4+
"description": "ESLint plugin that prevents the use of JSX class components",
5+
"license": "MIT",
6+
"author": "Tate <[email protected]>",
7+
"homepage": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component#readme",
8+
"repository": {
9+
"type": "git",
10+
"url": "git+https://github.com/tatethurston/eslint-plugin-react-prefer-function-component.git"
11+
},
12+
"bugs": {
13+
"url": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/issues"
14+
},
15+
"main": "index.js",
16+
"types": "index.d.ts",
17+
"keywords": [
18+
"eslint react no class",
19+
"eslint react class",
20+
"lint react no class",
21+
"lint react class",
22+
"lint jsx no class",
23+
"lint jsx class"
24+
]
25+
}

src/prefer-function-component/index.ts

+17-67
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44

55
import type { Rule } from "eslint";
66

7-
// TODO:
8-
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
9-
// https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/util/pragma.js
10-
const pragma = "React";
11-
const createClass = "createReactClass";
127
export const COMPONENT_SHOULD_BE_FUNCTION = "componentShouldBeFunction";
138
export const ALLOW_COMPONENT_DID_CATCH = "allowComponentDidCatch";
149
const COMPONENT_DID_CATCH = "componentDidCatch";
@@ -21,34 +16,6 @@ type Node = any;
2116

2217
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */
2318

24-
function getComponentProperties(node: Node): Node[] {
25-
switch (node.type) {
26-
case "ClassDeclaration":
27-
case "ClassExpression":
28-
return node.body.body;
29-
case "ObjectExpression":
30-
return node.properties;
31-
default:
32-
return [];
33-
}
34-
}
35-
36-
function getPropertyNameNode(node: Node): Node | undefined {
37-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
38-
if (node.key || ["MethodDefinition", "Property"].indexOf(node.type) !== -1) {
39-
return node.key;
40-
}
41-
if (node.type === "MemberExpression") {
42-
return node.property;
43-
}
44-
return undefined;
45-
}
46-
47-
function getPropertyName(node: Node): string {
48-
const nameNode = getPropertyNameNode(node);
49-
return nameNode ? nameNode.name : "";
50-
}
51-
5219
// https://eslint.org/docs/developer-guide/working-with-rules
5320
const rule: Rule.RuleModule = {
5421
meta: {
@@ -81,51 +48,34 @@ const rule: Rule.RuleModule = {
8148
create(context: Rule.RuleContext) {
8249
const allowComponentDidCatch =
8350
context.options[0]?.allowComponentDidCatch ?? true;
84-
const sourceCode = context.getSourceCode();
85-
86-
function isES5Component(node: Node): boolean {
87-
if (!node.parent) {
88-
return false;
89-
}
90-
91-
return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(
92-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
93-
sourceCode.getText(node.parent.callee)
94-
);
95-
}
96-
97-
function isES6Component(node: Node): boolean {
98-
if (!node.superClass) {
99-
return false;
100-
}
101-
102-
return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(
103-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
104-
sourceCode.getText(node.superClass)
105-
);
106-
}
10751

10852
function shouldPreferFunction(node: Node): boolean {
109-
if (!allowComponentDidCatch) {
110-
return true;
53+
const properties = node.body.body;
54+
const hasComponentDidCatch =
55+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
56+
properties.find(
57+
(property: Node) => property.key?.name === COMPONENT_DID_CATCH
58+
) !== undefined;
59+
60+
if (hasComponentDidCatch && allowComponentDidCatch) {
61+
return false;
11162
}
112-
113-
const properties = getComponentProperties(node).map(getPropertyName);
114-
return !properties.includes(COMPONENT_DID_CATCH);
63+
return true;
11564
}
11665

11766
const components = new Set<Node>();
11867

119-
const detect = (guard: (node: Node) => boolean) => (node: Node) => {
120-
if (guard(node) && shouldPreferFunction(node)) {
68+
function detect(node: Node): void {
69+
if (shouldPreferFunction(node)) {
12170
components.add(node);
12271
}
123-
};
72+
}
12473

12574
return {
126-
ObjectExpression: detect(isES5Component),
127-
ClassDeclaration: detect(isES6Component),
128-
ClassExpression: detect(isES6Component),
75+
"ClassDeclaration:has(JSXElement)": detect,
76+
"ClassDeclaration:has(JSXFragment)": detect,
77+
"ClassExpression:has(JSXElement)": detect,
78+
"ClassExpression:has(JSXFragment)": detect,
12979

13080
[PROGRAM_EXIT]() {
13181
components.forEach((node) => {

src/prefer-function-component/test.ts

+70
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,28 @@ ruleTester.run("prefer-function-component", rule, {
6767
};
6868
`,
6969
},
70+
{
71+
// class without JSX
72+
code: `
73+
class Foo {
74+
render() {
75+
return 'hello'
76+
}
77+
};
78+
`,
79+
},
80+
{
81+
// object with JSX
82+
code: `
83+
const foo = {
84+
foo: <h>hello</h>
85+
};
86+
`,
87+
},
7088
],
7189

7290
invalid: [
91+
// Extending from react
7392
{
7493
code: `
7594
import { Component } from 'react';
@@ -86,6 +105,57 @@ ruleTester.run("prefer-function-component", rule, {
86105
},
87106
],
88107
},
108+
// Extending from preact
109+
{
110+
code: `
111+
import { Component } from 'preact';
112+
113+
class Foo extends Component {
114+
render() {
115+
return <div>{this.props.foo}</div>;
116+
}
117+
}
118+
`,
119+
errors: [
120+
{
121+
messageId: COMPONENT_SHOULD_BE_FUNCTION,
122+
},
123+
],
124+
},
125+
// Extending from inferno
126+
{
127+
code: `
128+
import { Component } from 'inferno';
129+
130+
class Foo extends Component {
131+
render() {
132+
return <div>{this.props.foo}</div>;
133+
}
134+
}
135+
`,
136+
errors: [
137+
{
138+
messageId: COMPONENT_SHOULD_BE_FUNCTION,
139+
},
140+
],
141+
},
142+
// Extending from another class (not Component)
143+
{
144+
code: `
145+
import Document from 'next/document';
146+
147+
class Foo extends Document {
148+
render() {
149+
return <div>{this.props.foo}</div>;
150+
}
151+
}
152+
`,
153+
errors: [
154+
{
155+
messageId: COMPONENT_SHOULD_BE_FUNCTION,
156+
},
157+
],
158+
},
89159
{
90160
code: `
91161
class Foo extends React.Component {

0 commit comments

Comments
 (0)