Skip to content

Commit 67d62bd

Browse files
committed
Add void-dom-elements-no-children rule
There are some HTML elements that are only self-closing (e.g. `img`, `br`, `hr`). These are collectively known as void DOM elements. If you try to give these children, React will give you a warning like: > Invariant Violation: img is a void element tag and must neither have > `children` nor use `dangerouslySetInnerHTML`. This rule prevents this from happening. Since this is already a warning in React, we should add it to the recommended configuration in our next major release. Fixes #709
1 parent e6fdd02 commit 67d62bd

File tree

5 files changed

+269
-0
lines changed

5 files changed

+269
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
111111
* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order
112112
* [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting
113113
* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value being an object
114+
* [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md): Prevent void DOM elements (e.g. `<img />`, `<br />`) from receiving children
114115

115116
## JSX-specific rules
116117

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Prevent void DOM elements (e.g. `<img />`, `<br />`) from receiving children
2+
3+
There are some HTML elements that are only self-closing (e.g. `img`, `br`, `hr`). These are collectively known as void DOM elements. If you try to give these children, React will give you a warning like:
4+
5+
> Invariant Violation: img is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.
6+
7+
8+
## Rule Details
9+
10+
The following patterns are considered warnings:
11+
12+
```jsx
13+
<br>Children</br>
14+
<br children='Children' />
15+
<br dangerouslySetInnerHTML={{ __html: 'HTML' }} />
16+
React.createElement('br', undefined, 'Children')
17+
React.createElement('br', { children: 'Children' })
18+
React.createElement('br', { dangerouslySetInnerHTML: { __html: 'HTML' } })
19+
```
20+
21+
The following patterns are not considered warnings:
22+
23+
```jsx
24+
<div>Children</div>
25+
<div children='Children' />
26+
<div dangerouslySetInnerHTML={{ __html: 'HTML' }} />
27+
React.createElement('div', undefined, 'Children')
28+
React.createElement('div', { children: 'Children' })
29+
React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } })
30+
```

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ var allRules = {
5757
'style-prop-object': require('./lib/rules/style-prop-object'),
5858
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
5959
'no-children-prop': require('./lib/rules/no-children-prop'),
60+
'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children'),
6061
'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
6162
'require-extension': require('./lib/rules/require-extension'),
6263
'wrap-multilines': require('./lib/rules/wrap-multilines'),
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* @fileoverview Prevent void elements (e.g. <img />, <br />) from receiving
3+
* children
4+
* @author Joe Lencioni
5+
*/
6+
'use strict';
7+
8+
var find = require('array.prototype.find');
9+
var has = require('has');
10+
11+
// ------------------------------------------------------------------------------
12+
// Helpers
13+
// ------------------------------------------------------------------------------
14+
15+
// Using an object here to avoid array scan. We should switch to Set once
16+
// support is good enough.
17+
var VOID_DOM_ELEMENTS = {
18+
area: true,
19+
base: true,
20+
br: true,
21+
col: true,
22+
embed: true,
23+
hr: true,
24+
img: true,
25+
input: true,
26+
keygen: true,
27+
link: true,
28+
menuitem: true,
29+
meta: true,
30+
param: true,
31+
source: true,
32+
track: true,
33+
wbr: true
34+
};
35+
36+
function isVoidDOMElement(elementName) {
37+
return has(VOID_DOM_ELEMENTS, elementName);
38+
}
39+
40+
function errorMessage(elementName) {
41+
return 'Void DOM element <' + elementName + ' /> cannot receive children.';
42+
}
43+
44+
// ------------------------------------------------------------------------------
45+
// Rule Definition
46+
// ------------------------------------------------------------------------------
47+
48+
module.exports = {
49+
meta: {
50+
docs: {
51+
description: 'Prevent passing of children to void DOM elements (e.g. <br />).',
52+
category: 'Best Practices',
53+
recommended: false
54+
},
55+
schema: []
56+
},
57+
58+
create: function(context) {
59+
return {
60+
JSXElement: function(node) {
61+
var elementName = node.openingElement.name.name;
62+
63+
if (!isVoidDOMElement(elementName)) {
64+
// e.g. <div />
65+
return;
66+
}
67+
68+
if (node.children.length > 0) {
69+
// e.g. <br>Foo</br>
70+
context.report({
71+
node: node,
72+
message: errorMessage(elementName)
73+
});
74+
}
75+
76+
var attributes = node.openingElement.attributes;
77+
78+
var hasChildrenAttributeOrDanger = !!find(attributes, function(attribute) {
79+
if (!attribute.name) {
80+
return false;
81+
}
82+
83+
return attribute.name.name === 'children' || attribute.name.name === 'dangerouslySetInnerHTML';
84+
});
85+
86+
if (hasChildrenAttributeOrDanger) {
87+
// e.g. <br children="Foo" />
88+
context.report({
89+
node: node,
90+
message: errorMessage(elementName)
91+
});
92+
}
93+
},
94+
95+
CallExpression: function(node) {
96+
if (node.callee.type !== 'MemberExpression') {
97+
return;
98+
}
99+
100+
if (node.callee.property.name !== 'createElement') {
101+
return;
102+
}
103+
104+
var args = node.arguments;
105+
var elementName = args[0].value;
106+
107+
if (!isVoidDOMElement(elementName)) {
108+
// e.g. React.createElement('div');
109+
return;
110+
}
111+
112+
var firstChild = args[2];
113+
if (firstChild) {
114+
// e.g. React.createElement('br', undefined, 'Foo')
115+
context.report({
116+
node: node,
117+
message: errorMessage(elementName)
118+
});
119+
}
120+
121+
var props = args[1].properties;
122+
123+
var hasChildrenPropOrDanger = !!find(props, function(prop) {
124+
if (!prop.key) {
125+
return false;
126+
}
127+
128+
return prop.key.name === 'children' || prop.key.name === 'dangerouslySetInnerHTML';
129+
});
130+
131+
if (hasChildrenPropOrDanger) {
132+
// e.g. React.createElement('br', { children: 'Foo' })
133+
context.report({
134+
node: node,
135+
message: errorMessage(elementName)
136+
});
137+
}
138+
}
139+
};
140+
}
141+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @fileoverview Tests for void-dom-elements-no-children
3+
* @author Joe Lencioni
4+
*/
5+
6+
'use strict';
7+
8+
// -----------------------------------------------------------------------------
9+
// Requirements
10+
// -----------------------------------------------------------------------------
11+
12+
var rule = require('../../../lib/rules/void-dom-elements-no-children');
13+
var RuleTester = require('eslint').RuleTester;
14+
15+
var parserOptions = {
16+
ecmaVersion: 6,
17+
ecmaFeatures: {
18+
experimentalObjectRestSpread: true,
19+
jsx: true
20+
}
21+
};
22+
23+
function errorMessage(elementName) {
24+
return 'Void DOM element <' + elementName + ' /> cannot receive children.';
25+
}
26+
27+
// -----------------------------------------------------------------------------
28+
// Tests
29+
// -----------------------------------------------------------------------------
30+
31+
var ruleTester = new RuleTester();
32+
ruleTester.run('void-dom-elements-no-children', rule, {
33+
valid: [
34+
{
35+
code: '<div>Foo</div>;',
36+
parserOptions: parserOptions
37+
},
38+
{
39+
code: '<div children="Foo" />;',
40+
parserOptions: parserOptions
41+
},
42+
{
43+
code: '<div dangerouslySetInnerHTML={{ __html: "Foo" }} />;',
44+
parserOptions: parserOptions
45+
},
46+
{
47+
code: 'React.createElement("div", {}, "Foo");',
48+
parserOptions: parserOptions
49+
},
50+
{
51+
code: 'React.createElement("div", { children: "Foo" });',
52+
parserOptions: parserOptions
53+
},
54+
{
55+
code: 'React.createElement("div", { dangerouslySetInnerHTML: { __html: "Foo" } });',
56+
parserOptions: parserOptions
57+
}
58+
],
59+
invalid: [
60+
{
61+
code: '<br>Foo</br>;',
62+
errors: [{message: errorMessage('br')}],
63+
parserOptions: parserOptions
64+
},
65+
{
66+
code: '<br children="Foo" />;',
67+
errors: [{message: errorMessage('br')}],
68+
parserOptions: parserOptions
69+
},
70+
{
71+
code: '<img {...props} children="Foo" />;',
72+
errors: [{message: errorMessage('img')}],
73+
parserOptions: parserOptions
74+
},
75+
{
76+
code: '<br dangerouslySetInnerHTML={{ __html: "Foo" }} />;',
77+
errors: [{message: errorMessage('br')}],
78+
parserOptions: parserOptions
79+
},
80+
{
81+
code: 'React.createElement("br", {}, "Foo");',
82+
errors: [{message: errorMessage('br')}],
83+
parserOptions: parserOptions
84+
},
85+
{
86+
code: 'React.createElement("br", { children: "Foo" });',
87+
errors: [{message: errorMessage('br')}],
88+
parserOptions: parserOptions
89+
},
90+
{
91+
code: 'React.createElement("br", { dangerouslySetInnerHTML: { __html: "Foo" } });',
92+
errors: [{message: errorMessage('br')}],
93+
parserOptions: parserOptions
94+
}
95+
]
96+
});

0 commit comments

Comments
 (0)