Skip to content

Commit c2b155e

Browse files
authored
new option: allowJsxUtilityClass (#12)
1 parent b1f7961 commit c2b155e

File tree

5 files changed

+110
-39
lines changed

5 files changed

+110
-39
lines changed

CHANGELOG.md

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

3+
## v3.1.0
4+
5+
- New option: `allowJsxUtilityClass`. This configuration option permits JSX utility classes: classes that have methods that return JSX but are not themselves components(they do not extend from a Component class or have a render method).
6+
7+
The following is now permitted when enabling this configuration option:
8+
9+
```jsx
10+
class Foo {
11+
getBar() {
12+
return <Bar />;
13+
}
14+
}
15+
```
16+
17+
Thanks [noahm](https://github.com/noahm) for the contribution!
18+
319
## v3.0.0
420

521
Detects `class` components that extend the `Component` class, even if they do not use any JSX. Now errors on manager, business logic, and other renderless `class` components that extend `Component`. Previously the below was not caught:

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,36 @@ class Foo extends Component {
162162
}
163163
```
164164

165+
### `allowJsxUtilityClass`
166+
167+
When `true` the rule will ignore JS classes that aren't class Components
168+
169+
Examples of **correct** code for this rule:
170+
171+
```jsx
172+
import { Bar } from "./Bar";
173+
174+
class Foo {
175+
getBar() {
176+
return <Bar />;
177+
}
178+
}
179+
```
180+
181+
When `false` (the default) the rule will flag any class with JSX
182+
183+
Examples of **incorrect** code for this rule:
184+
185+
```jsx
186+
import { Bar } from "./Bar";
187+
188+
class Foo {
189+
getBar() {
190+
return <Bar />;
191+
}
192+
}
193+
```
194+
165195
## Contributing 👫
166196

167197
PR's and issues welcomed! For more guidance check out [CONTRIBUTING.md](https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/blob/master/CONTRIBUTING.md)

public.package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-react-prefer-function-component",
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"description": "ESLint plugin that prevents the use of JSX class components",
55
"license": "MIT",
66
"author": "Tate <[email protected]>",

src/prefer-function-component/index.ts

+28-15
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import type { Rule } from "eslint";
66

77
export const COMPONENT_SHOULD_BE_FUNCTION = "componentShouldBeFunction";
88
export const ALLOW_COMPONENT_DID_CATCH = "allowComponentDidCatch";
9-
export const ALLOW_JSX_IN_CLASSES = "allowJsxInClasses";
10-
const COMPONENT_DID_CATCH = "componentDidCatch";
9+
export const ALLOW_JSX_UTILITY_CLASS = "allowJsxUtilityClass";
1110
// https://eslint.org/docs/developer-guide/working-with-rules
1211
const PROGRAM_EXIT = "Program:exit";
1312
const VARIABLE_DECLARATOR = "VariableDeclarator";
@@ -41,7 +40,7 @@ const rule: Rule.RuleModule = {
4140
default: true,
4241
type: "boolean",
4342
},
44-
[ALLOW_JSX_IN_CLASSES]: {
43+
[ALLOW_JSX_UTILITY_CLASS]: {
4544
default: false,
4645
type: "boolean",
4746
},
@@ -54,15 +53,15 @@ const rule: Rule.RuleModule = {
5453
create(context: Rule.RuleContext) {
5554
const allowComponentDidCatch =
5655
context.options[0]?.allowComponentDidCatch ?? true;
57-
// const allowJsxInClasses =
58-
// context.options[0]?.allowComponentDidCatch ?? false;
56+
const allowJsxUtilityClass =
57+
context.options[0]?.allowJsxUtilityClass ?? false;
5958

6059
function shouldPreferFunction(node: Node): boolean {
6160
const properties = node.body.body;
6261
const hasComponentDidCatch =
6362
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
6463
properties.find(
65-
(property: Node) => property.key?.name === COMPONENT_DID_CATCH
64+
(property: Node) => property.key?.name === "componentDidCatch"
6665
) !== undefined;
6766

6867
if (hasComponentDidCatch && allowComponentDidCatch) {
@@ -79,23 +78,37 @@ const rule: Rule.RuleModule = {
7978
}
8079
}
8180

82-
// function detectJsxInClass(node: Node): void {
83-
// if (!allowJsxInClasses) {
84-
// detect(node);
85-
// }
86-
// }
81+
function detectJsxInClass(node: Node): void {
82+
if (!allowJsxUtilityClass) {
83+
detect(node);
84+
}
85+
}
8786

8887
return {
89-
"ClassDeclaration:has(JSXElement)": detect,
90-
"ClassDeclaration:has(JSXFragment)": detect,
88+
"ClassDeclaration:has(JSXElement)": detectJsxInClass,
89+
"ClassDeclaration:has(JSXFragment)": detectJsxInClass,
90+
"ClassExpression:has(JSXElement)": detectJsxInClass,
91+
"ClassExpression:has(JSXFragment)": detectJsxInClass,
92+
"ClassDeclaration:has(JSXElement):has(MethodDefinition[key.name='render'])":
93+
detect,
94+
"ClassDeclaration:has(JSXFragment):has(MethodDefinition[key.name='render'])":
95+
detect,
96+
"ClassExpression:has(JSXElement):has(MethodDefinition[key.name='render'])":
97+
detect,
98+
"ClassExpression:has(JSXFragment):has(MethodDefinition[key.name='render'])":
99+
detect,
91100
"ClassDeclaration[superClass.object.name='React'][superClass.property.name='Component']":
92101
detect,
102+
"ClassDeclaration[superClass.object.name='React'][superClass.property.name='PureComponent']":
103+
detect,
93104
"ClassDeclaration[superClass.name='Component']": detect,
94-
"ClassExpression:has(JSXElement)": detect,
95-
"ClassExpression:has(JSXFragment)": detect,
105+
"ClassDeclaration[superClass.name='PureComponent']": detect,
96106
"ClassExpression[superClass.object.name='React'][superClass.property.name='Component']":
97107
detect,
108+
"ClassExpression[superClass.object.name='React'][superClass.property.name='PureComponent']":
109+
detect,
98110
"ClassExpression[superClass.name='Component']": detect,
111+
"ClassExpression[superClass.name='PureComponent']": detect,
99112
[PROGRAM_EXIT]() {
100113
components.forEach((node) => {
101114
// report on just the class identifier

src/prefer-function-component/test.ts

+35-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RuleTester } from "eslint";
22
import rule, {
33
ALLOW_COMPONENT_DID_CATCH,
4-
ALLOW_JSX_IN_CLASSES,
4+
ALLOW_JSX_UTILITY_CLASS,
55
COMPONENT_SHOULD_BE_FUNCTION,
66
} from ".";
77

@@ -176,38 +176,39 @@ const componentDidCatch = [
176176
`,
177177
];
178178

179+
const jsxUtilityClass = [
180+
`\
181+
class Foo {
182+
getBar() {
183+
return <Bar />;
184+
}
185+
};
186+
`,
187+
];
188+
179189
ruleTester.run("prefer-function-component", rule, {
180190
valid: [
181191
...validForAllOptions.flatMap((code) => [
182192
{ code },
183-
{ code, options: [{ [ALLOW_JSX_IN_CLASSES]: true }] },
184-
{ code, options: [{ [ALLOW_COMPONENT_DID_CATCH]: true }] },
193+
{ code, options: [{ [ALLOW_JSX_UTILITY_CLASS]: true }] },
194+
{ code, options: [{ [ALLOW_COMPONENT_DID_CATCH]: false }] },
185195
{
186196
code,
187197
options: [
188-
{ [ALLOW_JSX_IN_CLASSES]: true, [ALLOW_COMPONENT_DID_CATCH]: true },
198+
{
199+
[ALLOW_JSX_UTILITY_CLASS]: true,
200+
[ALLOW_COMPONENT_DID_CATCH]: false,
201+
},
189202
],
190203
},
191204
]),
192205
...componentDidCatch.flatMap((code) => [
193206
{ code },
194-
{ code, options: [{ [ALLOW_JSX_IN_CLASSES]: true }] },
207+
{ code, options: [{ [ALLOW_JSX_UTILITY_CLASS]: true }] },
208+
]),
209+
...jsxUtilityClass.flatMap((code) => [
210+
{ code, options: [{ [ALLOW_JSX_UTILITY_CLASS]: true }] },
195211
]),
196-
// {
197-
// // non-component class with JSX
198-
// code: `
199-
// class Foo {
200-
// getBar() {
201-
// return <Bar />;
202-
// }
203-
// };
204-
// `,
205-
// options: [
206-
// {
207-
// [ALLOW_JSX_IN_CLASSES]: true,
208-
// },
209-
// ],
210-
// },
211212
],
212213

213214
invalid: [
@@ -216,18 +217,21 @@ ruleTester.run("prefer-function-component", rule, {
216217
{
217218
code,
218219
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
219-
options: [{ [ALLOW_JSX_IN_CLASSES]: true }],
220+
options: [{ [ALLOW_JSX_UTILITY_CLASS]: true }],
220221
},
221222
{
222223
code,
223224
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
224-
options: [{ [ALLOW_COMPONENT_DID_CATCH]: true }],
225+
options: [{ [ALLOW_COMPONENT_DID_CATCH]: false }],
225226
},
226227
{
227228
code,
228229
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
229230
options: [
230-
{ [ALLOW_JSX_IN_CLASSES]: true, [ALLOW_COMPONENT_DID_CATCH]: true },
231+
{
232+
[ALLOW_JSX_UTILITY_CLASS]: true,
233+
[ALLOW_COMPONENT_DID_CATCH]: false,
234+
},
231235
],
232236
},
233237
]),
@@ -244,5 +248,13 @@ ruleTester.run("prefer-function-component", rule, {
244248
},
245249
],
246250
})),
251+
...jsxUtilityClass.flatMap((code) => [
252+
{ code, errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }] },
253+
{
254+
code,
255+
errors: [{ messageId: COMPONENT_SHOULD_BE_FUNCTION }],
256+
options: [{ [ALLOW_COMPONENT_DID_CATCH]: false }],
257+
},
258+
]),
247259
],
248260
});

0 commit comments

Comments
 (0)