Skip to content

Commit 1d413d5

Browse files
authored
Merge pull request #12 from SukkaW/react-no-children-in-void-dom-elements
2 parents a74c433 + 3a6909b commit 1d413d5

File tree

5 files changed

+358
-3
lines changed

5 files changed

+358
-3
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"mode": "location"
3636
}
3737
],
38+
"eslint.experimental.useFlatConfig": false,
3839
"search.exclude": {
3940
"pnpm-lock.yaml": true
4041
},

packages/eslint-plugin-jsx/src/rules/no-children-in-void-dom-elements.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# @eslint-react/no-children-in-void-dom-elements
2+
3+
Self-closing HTML elements (e.g. `<img />`, `<br />`, `<hr />`) are collectively known as void DOM elements. React will give you a warning if you try to give these children:
4+
5+
> Invariant Violation: img is a void element tag and must neither have children nor use dangerouslySetInnerHTML.
6+
7+
### ❌ Incorrect
8+
9+
```tsx
10+
<br>Children</br>
11+
<br children="Children" />
12+
<br dangerouslySetInnerHTML={{ __html: 'HTML' }} />
13+
React.createElement('br', undefined, 'Children')
14+
React.createElement('br', { children: 'Children' })
15+
React.createElement('br', { dangerouslySetInnerHTML: { __html: 'HTML' } })
16+
```
17+
18+
### ✅ Correct
19+
20+
```tsx
21+
<div>Children</div>
22+
<div children="Children" />
23+
<div dangerouslySetInnerHTML={{ __html: 'HTML' }} />
24+
React.createElement('div', undefined, 'Children')
25+
React.createElement('div', { children: 'Children' })
26+
React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } })
27+
```
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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-in-void-dom-elements";
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>Foo</div>;",
26+
'<div children="Foo" />;',
27+
28+
'<div dangerouslySetInnerHTML={{ __html: "Foo" }} />;',
29+
30+
'React.createElement("div", {}, "Foo");',
31+
32+
'React.createElement("div", { children: "Foo" });',
33+
34+
'React.createElement("div", { dangerouslySetInnerHTML: { __html: "Foo" } });',
35+
36+
'document.createElement("img");',
37+
38+
'React.createElement("img");',
39+
40+
"React.createElement();",
41+
42+
"document.createElement();",
43+
44+
dedent`
45+
const props = {};
46+
React.createElement("img", props);
47+
`,
48+
49+
dedent`
50+
import React, {createElement} from "react";
51+
createElement("div");
52+
`,
53+
54+
dedent`
55+
import React, {createElement} from "react";
56+
createElement("img");
57+
`,
58+
59+
dedent`
60+
import React, {createElement, PureComponent} from "react";
61+
class Button extends PureComponent {
62+
handleClick(ev) {
63+
ev.preventDefault();
64+
}
65+
render() {
66+
return <div onClick={this.handleClick}>Hello</div>;
67+
}
68+
}
69+
`,
70+
],
71+
invalid: [
72+
{
73+
code: "<br>Foo</br>;",
74+
errors: [
75+
{
76+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
77+
data: { element: "br" },
78+
},
79+
],
80+
},
81+
{
82+
code: '<br children="Foo" />;',
83+
errors: [
84+
{
85+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
86+
data: { element: "br" },
87+
},
88+
],
89+
},
90+
{
91+
code: '<img {...props} children="Foo" />;',
92+
errors: [
93+
{
94+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
95+
data: { element: "img" },
96+
},
97+
],
98+
},
99+
{
100+
code: '<br dangerouslySetInnerHTML={{ __html: "Foo" }} />;',
101+
errors: [
102+
{
103+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
104+
data: { element: "br" },
105+
},
106+
],
107+
},
108+
{
109+
code: 'React.createElement("br", {}, "Foo");',
110+
errors: [
111+
{
112+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
113+
data: { element: "br" },
114+
},
115+
],
116+
},
117+
{
118+
code: 'React.createElement("br", { children: "Foo" });',
119+
errors: [
120+
{
121+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
122+
data: { element: "br" },
123+
},
124+
],
125+
},
126+
{
127+
code: 'React.createElement("br", { dangerouslySetInnerHTML: { __html: "Foo" } });',
128+
errors: [
129+
{
130+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
131+
data: { element: "br" },
132+
},
133+
],
134+
},
135+
{
136+
code: dedent`
137+
import React, {createElement} from "react";
138+
createElement("img", {}, "Foo");
139+
`,
140+
errors: [
141+
{
142+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
143+
data: { element: "img" },
144+
},
145+
],
146+
},
147+
{
148+
code: dedent`
149+
import React, {createElement} from "react";
150+
createElement("img", { children: "Foo" });
151+
`,
152+
errors: [
153+
{
154+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
155+
data: { element: "img" },
156+
},
157+
],
158+
},
159+
{
160+
code: dedent`
161+
import React, {createElement} from "react";
162+
createElement("img", { dangerouslySetInnerHTML: { __html: "Foo" } });
163+
`,
164+
errors: [
165+
{
166+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
167+
data: { element: "img" },
168+
},
169+
],
170+
},
171+
],
172+
});
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { NodeType } from "@eslint-react/ast";
2+
import { isCreateElement } from "@eslint-react/create-element";
3+
import { createRule } from "@eslint-react/shared";
4+
5+
export const RULE_NAME = "no-children-in-void-dom-elements";
6+
7+
type MessageID = "NO_CHILDREN_IN_VOID_DOM_ELEMENTS";
8+
9+
const voidElements = new Set([
10+
"area",
11+
"base",
12+
"br",
13+
"col",
14+
"embed",
15+
"hr",
16+
"img",
17+
"input",
18+
"keygen",
19+
"link",
20+
"menuitem",
21+
"meta",
22+
"param",
23+
"source",
24+
"track",
25+
"wbr",
26+
]);
27+
28+
export default createRule<[], MessageID>({
29+
name: RULE_NAME,
30+
meta: {
31+
type: "problem",
32+
docs: {
33+
description: "disallows passing children to void DOM elements",
34+
recommended: "recommended",
35+
requiresTypeChecking: false,
36+
},
37+
schema: [],
38+
messages: {
39+
NO_CHILDREN_IN_VOID_DOM_ELEMENTS: "Void DOM elements <{{element}} /> cannot have children.",
40+
},
41+
},
42+
defaultOptions: [],
43+
create(context) {
44+
return {
45+
CallExpression(node) {
46+
if (
47+
node.callee.type !== NodeType.MemberExpression
48+
&& node.callee.type !== NodeType.Identifier
49+
) {
50+
return;
51+
}
52+
53+
if (!isCreateElement(node, context)) {
54+
return;
55+
}
56+
57+
const args = node.arguments;
58+
59+
if (args.length < 2) {
60+
// React.createElement() with only one argument is valid (definitely no children)
61+
return;
62+
}
63+
64+
const elementNameNode = args[0];
65+
if (!elementNameNode || !("value" in elementNameNode)) {
66+
return;
67+
}
68+
69+
const elementName = elementNameNode.value;
70+
if (typeof elementName !== "string" || !voidElements.has(elementName)) {
71+
return;
72+
}
73+
74+
const propsNode = args[1];
75+
if (propsNode?.type !== NodeType.ObjectExpression) {
76+
return;
77+
}
78+
79+
const firstChild = args[2];
80+
if (firstChild) {
81+
// e.g. React.createElement('br', undefined, 'Foo')
82+
context.report({
83+
data: {
84+
element: elementName,
85+
},
86+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
87+
node,
88+
});
89+
}
90+
91+
const props = propsNode.properties;
92+
93+
const hasChildrenPropOrDanger = props.some((prop) => {
94+
if (!("key" in prop)) {
95+
return false;
96+
}
97+
if (!("name" in prop.key)) {
98+
return false;
99+
}
100+
101+
return prop.key.name === "children" || prop.key.name === "dangerouslySetInnerHTML";
102+
});
103+
104+
if (hasChildrenPropOrDanger) {
105+
// e.g. React.createElement('br', { children: 'Foo' })
106+
context.report({
107+
data: {
108+
element: elementName,
109+
},
110+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
111+
node,
112+
});
113+
}
114+
},
115+
JSXElement(node) {
116+
const openingElementNameExpression = node.openingElement.name;
117+
if ("name" in openingElementNameExpression) {
118+
const elementName = openingElementNameExpression.name;
119+
120+
if (typeof elementName !== "string" || !voidElements.has(elementName)) {
121+
return;
122+
}
123+
124+
if (node.children.length > 0) {
125+
context.report({
126+
data: {
127+
element: elementName,
128+
},
129+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
130+
node,
131+
});
132+
}
133+
134+
const { attributes } = node.openingElement;
135+
136+
const hasChildrenAttributeOrDanger = attributes.some((attribute) => {
137+
if (!("name" in attribute)) {
138+
return false;
139+
}
140+
141+
return attribute.name.name === "children" || attribute.name.name === "dangerouslySetInnerHTML";
142+
});
143+
144+
if (hasChildrenAttributeOrDanger) {
145+
// e.g. <br children="Foo" />
146+
context.report({
147+
data: {
148+
element: elementName,
149+
},
150+
messageId: "NO_CHILDREN_IN_VOID_DOM_ELEMENTS",
151+
node,
152+
});
153+
}
154+
}
155+
},
156+
};
157+
},
158+
});

0 commit comments

Comments
 (0)