Skip to content

Commit eb6da17

Browse files
committed
feat: add no-children-prop, closes #101
1 parent f553ba3 commit eb6da17

File tree

10 files changed

+271
-4
lines changed

10 files changed

+271
-4
lines changed

packages/eslint-plugin-react/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import noChildrenForEach from "./rules/no-children-for-each";
66
import noChildrenInVoidDomElements from "./rules/no-children-in-void-dom-elements";
77
import noChildrenMap from "./rules/no-children-map";
88
import noChildrenOnly from "./rules/no-children-only";
9+
import noChildrenProp from "./rules/no-children-prop";
910
import noChildrenToArray from "./rules/no-children-to-array";
1011
import noClassComponent from "./rules/no-class-component";
1112
import noCloneElement from "./rules/no-clone-element";
@@ -35,6 +36,7 @@ export const rules = {
3536
"no-children-in-void-dom-elements": noChildrenInVoidDomElements,
3637
"no-children-map": noChildrenMap,
3738
"no-children-only": noChildrenOnly,
39+
"no-children-prop": noChildrenProp,
3840
"no-children-to-array": noChildrenToArray,
3941
"no-class-component": noClassComponent,
4042
"no-clone-element": noCloneElement,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# react/no-children-prop
2+
3+
<!-- end auto-generated rule header -->
4+
5+
## Rule category
6+
7+
Suspicious.
8+
9+
## What it does
10+
11+
Disallows passing of children as props.
12+
13+
## Why is this bad?
14+
15+
Most of the time, `children` should be actual `children`, not passed in as a `prop`.
16+
17+
When using JSX, the `children` should be nested between the opening and closing tags. When not using JSX, the `children` should be passed as additional arguments to `React.createElement`.
18+
19+
## Examples
20+
21+
### ❌ Incorrect
22+
23+
```tsx
24+
<div children='Children' />
25+
26+
<Component children={<AnotherComponent />} />
27+
<Component children={['Child 1', 'Child 2']} />
28+
29+
React.createElement("div", { children: 'Children' })
30+
```
31+
32+
### ✅ Correct
33+
34+
```tsx
35+
<div>Children</div>
36+
37+
<Component>Children</Component>
38+
39+
<Component>
40+
<span>Child 1</span>
41+
<span>Child 2</span>
42+
</Component>
43+
44+
React.createElement("div", {}, 'Children')
45+
React.createElement("div", 'Child 1', 'Child 2')
46+
```
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { allValid } from "@eslint-react/shared";
2+
import dedent from "dedent";
3+
4+
import RuleTester, { getFixturesRootDir } from "../../../../test/rule-tester";
5+
import rule, { RULE_NAME } from "./no-children-prop";
6+
7+
const rootDir = getFixturesRootDir();
8+
9+
const ruleTester = new RuleTester({
10+
parser: "@typescript-eslint/parser",
11+
parserOptions: {
12+
ecmaFeatures: {
13+
jsx: true,
14+
},
15+
ecmaVersion: 2021,
16+
sourceType: "module",
17+
project: "./tsconfig.json",
18+
tsconfigRootDir: rootDir,
19+
},
20+
});
21+
22+
ruleTester.run(RULE_NAME, rule, {
23+
valid: [
24+
...allValid,
25+
"<div />;",
26+
"<div></div>;",
27+
'React.createElement("div", {});',
28+
'React.createElement("div", undefined);',
29+
'<div className="class-name"></div>;',
30+
'React.createElement("div", {className: "class-name"});',
31+
"<div>Children</div>;",
32+
'React.createElement("div", "Children");',
33+
'React.createElement("div", {}, "Children");',
34+
'React.createElement("div", undefined, "Children");',
35+
'<div className="class-name">Children</div>;',
36+
'React.createElement("div", {className: "class-name"}, "Children");',
37+
"<div><div /></div>;",
38+
'React.createElement("div", React.createElement("div"));',
39+
'React.createElement("div", {}, React.createElement("div"));',
40+
'React.createElement("div", undefined, React.createElement("div"));',
41+
"<div><div /><div /></div>;",
42+
'React.createElement("div", React.createElement("div"), React.createElement("div"));',
43+
'React.createElement("div", {}, React.createElement("div"), React.createElement("div"));',
44+
'React.createElement("div", undefined, React.createElement("div"), React.createElement("div"));',
45+
'React.createElement("div", [React.createElement("div"), React.createElement("div")]);',
46+
'React.createElement("div", {}, [React.createElement("div"), React.createElement("div")]);',
47+
'React.createElement("div", undefined, [React.createElement("div"), React.createElement("div")]);',
48+
"<MyComponent />",
49+
"React.createElement(MyComponent);",
50+
"React.createElement(MyComponent, {});",
51+
"React.createElement(MyComponent, undefined);",
52+
"<MyComponent>Children</MyComponent>;",
53+
'React.createElement(MyComponent, "Children");',
54+
'React.createElement(MyComponent, {}, "Children");',
55+
'React.createElement(MyComponent, undefined, "Children");',
56+
'<MyComponent className="class-name"></MyComponent>;',
57+
'React.createElement(MyComponent, {className: "class-name"});',
58+
'<MyComponent className="class-name">Children</MyComponent>;',
59+
'React.createElement(MyComponent, {className: "class-name"}, "Children");',
60+
'<MyComponent className="class-name" {...props} />;',
61+
'React.createElement(MyComponent, {className: "class-name", ...props});',
62+
],
63+
invalid: [
64+
{
65+
code: "<div children />;", // not a valid use case but make sure we don't crash
66+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
67+
},
68+
{
69+
code: '<div children="Children" />;',
70+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
71+
},
72+
{
73+
code: "<div children={<div />} />;",
74+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
75+
},
76+
{
77+
code: "<div children={[<div />, <div />]} />;",
78+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
79+
},
80+
{
81+
code: '<div children="Children">Children</div>;',
82+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
83+
},
84+
{
85+
code: 'React.createElement("div", {children: "Children"});',
86+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
87+
},
88+
{
89+
code: 'React.createElement("div", {children: "Children"}, "Children");',
90+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
91+
},
92+
{
93+
code: 'React.createElement("div", {children: React.createElement("div")});',
94+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
95+
},
96+
{
97+
code: 'React.createElement("div", {children: [React.createElement("div"), React.createElement("div")]});',
98+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
99+
},
100+
{
101+
code: '<MyComponent children="Children" />',
102+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
103+
},
104+
{
105+
code: 'React.createElement(MyComponent, {children: "Children"});',
106+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
107+
},
108+
{
109+
code: '<MyComponent className="class-name" children="Children" />;',
110+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
111+
},
112+
{
113+
code: 'React.createElement(MyComponent, {children: "Children", className: "class-name"});',
114+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
115+
},
116+
{
117+
code: '<MyComponent {...props} children="Children" />;',
118+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
119+
},
120+
{
121+
code: 'React.createElement(MyComponent, {...props, children: "Children"})',
122+
errors: [{ messageId: "NO_CHILDREN_PROP" }],
123+
},
124+
],
125+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { NodeType } from "@eslint-react/ast";
2+
import { findPropInProperties, getProp, isCreateElementCall } from "@eslint-react/jsx";
3+
import { O } from "@eslint-react/tools";
4+
import type { ESLintUtils } from "@typescript-eslint/utils";
5+
import type { ConstantCase } from "string-ts";
6+
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "no-children-prop";
10+
11+
export type MessageID = ConstantCase<typeof RULE_NAME>;
12+
13+
// No need to check because TypeScript don't allow this
14+
// function isValidAttribute(
15+
// prop: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute,
16+
// ) {
17+
// return (
18+
// "value" in prop
19+
// && prop.value
20+
// && "expression" in prop.value
21+
// && isFunction(prop.value.expression)
22+
// );
23+
// }
24+
25+
// No need to check because TypeScript don't allow this
26+
// function isValidProperty(
27+
// prop:
28+
// | TSESTree.PropertyComputedName
29+
// | TSESTree.PropertyNonComputedName
30+
// | TSESTree.RestElement
31+
// | TSESTree.SpreadElement,
32+
// ) {
33+
// return (
34+
// "value" in prop
35+
// && prop.value
36+
// && "type" in prop.value
37+
// && isFunction(prop.value)
38+
// );
39+
// }
40+
41+
export default createRule<[], MessageID>({
42+
name: RULE_NAME,
43+
meta: {
44+
type: "suggestion",
45+
docs: {
46+
description: "disallow passing of children as props",
47+
recommended: "recommended",
48+
requiresTypeChecking: false,
49+
},
50+
schema: [],
51+
messages: {
52+
NO_CHILDREN_PROP: "Children should always be actual children, not passed in as a prop.",
53+
},
54+
},
55+
defaultOptions: [],
56+
create(context) {
57+
return {
58+
JSXElement(node) {
59+
O.map(getProp(node.openingElement.attributes, "children", context), prop => {
60+
context.report({
61+
messageId: "NO_CHILDREN_PROP",
62+
node: prop,
63+
});
64+
});
65+
},
66+
// eslint-disable-next-line perfectionist/sort-objects
67+
CallExpression(node) {
68+
if (node.arguments.length === 0) {
69+
return;
70+
}
71+
72+
if (!isCreateElementCall(node, context)) {
73+
return;
74+
}
75+
76+
const [_, props] = node.arguments;
77+
78+
if (!props || props.type !== NodeType.ObjectExpression) {
79+
return;
80+
}
81+
82+
O.map(findPropInProperties(props.properties, context)("children"), prop => {
83+
context.report({
84+
messageId: "NO_CHILDREN_PROP",
85+
node: prop,
86+
});
87+
});
88+
},
89+
};
90+
},
91+
});

packages/eslint-plugin-react/src/rules/no-unsafe-target-blank.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { NodeType } from "@eslint-react/ast";
2-
import { getPropValue } from "@eslint-react/jsx";
2+
import { findPropInAttributes, getPropValue } from "@eslint-react/jsx";
33
import { F, O } from "@eslint-react/tools";
44
import { ESLintUtils } from "@typescript-eslint/utils";
55
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
66
import { isString } from "effect/Predicate";
77
import type { ConstantCase } from "string-ts";
88

9-
import { findPropInAttributes } from "../../../jsx/src/prop/find-prop-in-attributes";
109
import { createRule } from "../utils";
1110

1211
export const RULE_NAME = "no-unsafe-target-blank";

packages/eslint-plugin/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const rulePreset = {
3939
"react/no-children-in-void-dom-elements": "warn",
4040
"react/no-children-map": "warn",
4141
"react/no-children-only": "warn",
42+
"react/no-children-prop": "warn",
4243
"react/no-children-to-array": "warn",
4344
"react/no-class-component": "warn",
4445
"react/no-clone-element": "warn",
@@ -73,6 +74,7 @@ const recommendedPreset = {
7374
"naming-convention/component-name": "warn",
7475

7576
"react/no-children-in-void-dom-elements": "warn",
77+
"react/no-children-prop": "warn",
7678
"react/no-class-component": "warn",
7779
"react/no-clone-element": "warn",
7880
"react/no-constructed-context-value": "error",

packages/jsx/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ A function that searches for a property in the given properties
326326

327327
| Name | Type |
328328
| :--------- | :------------------------------------------------------------ |
329-
| `props` | `JSXAttribute`[] |
329+
| `props` | (`JSXAttribute` \| `JSXSpreadAttribute`)[] |
330330
| `propName` | `string` |
331331
| `context` | `Readonly`\<`RuleContext`\<`string`, readonly `unknown`[]\>\> |
332332

packages/jsx/src/prop/get-prop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { ESLintUtils } from "@typescript-eslint/utils";
77
import { findPropInAttributes } from "./find-prop-in-attributes";
88

99
export function getProp(
10-
props: TSESTree.JSXAttribute[],
10+
props: (TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute)[],
1111
propName: string,
1212
context: RuleContext,
1313
): O.Option<TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute> {

website/pages/rules/_meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"react-no-children-in-void-dom-elements": "react/no-children-in-void-dom-elements",
1818
"react-no-children-map": "react/no-children-map",
1919
"react-no-children-only": "react/no-children-only",
20+
"react-no-children-prop": "react/no-children-prop",
2021
"react-no-children-to-array": "react/no-children-to-array",
2122
"react-no-class-component": "react/no-class-component",
2223
"react-no-clone-element": "react/no-clone-element",

website/pages/rules/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
| [react/no-children-in-void-dom-elements](react-no-children-in-void-dom-elements) | disallow passing children to void DOM elements |
2626
| [react/no-children-map](react-no-children-map) | disallow `Children.map` |
2727
| [react/no-children-only](react-no-children-only) | disallow `Children.only()` |
28+
| [react/no-children-prop](react-no-children-prop) | disallow passing of children as props |
2829
| [react/no-children-to-array](react-no-children-to-array) | disallow `Children.toArray()` |
2930
| [react/no-class-component](react-no-class-component) | enforce that there are no class components |
3031
| [react/no-clone-element](react-no-clone-element) | disallow `cloneElement` |

0 commit comments

Comments
 (0)