Skip to content

Add void-dom-elements-no-children rule #1051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order
* [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting
* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value being an object
* [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

## JSX-specific rules

Expand Down
30 changes: 30 additions & 0 deletions docs/rules/void-dom-elements-no-children.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Prevent void DOM elements (e.g. `<img />`, `<br />`) from receiving children

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`.


## Rule Details

The following patterns are considered warnings:

```jsx
<br>Children</br>
<br children='Children' />
<br dangerouslySetInnerHTML={{ __html: 'HTML' }} />
React.createElement('br', undefined, 'Children')
React.createElement('br', { children: 'Children' })
React.createElement('br', { dangerouslySetInnerHTML: { __html: 'HTML' } })
```

The following patterns are not considered warnings:

```jsx
<div>Children</div>
<div children='Children' />
<div dangerouslySetInnerHTML={{ __html: 'HTML' }} />
React.createElement('div', undefined, 'Children')
React.createElement('div', { children: 'Children' })
React.createElement('div', { dangerouslySetInnerHTML: { __html: 'HTML' } })
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ var allRules = {
'style-prop-object': require('./lib/rules/style-prop-object'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-children-prop': require('./lib/rules/no-children-prop'),
'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children'),
'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
'require-extension': require('./lib/rules/require-extension'),
'wrap-multilines': require('./lib/rules/wrap-multilines'),
Expand Down
141 changes: 141 additions & 0 deletions lib/rules/void-dom-elements-no-children.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @fileoverview Prevent void elements (e.g. <img />, <br />) from receiving
* children
* @author Joe Lencioni
*/
'use strict';

var find = require('array.prototype.find');
var has = require('has');

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

// Using an object here to avoid array scan. We should switch to Set once
// support is good enough.
var VOID_DOM_ELEMENTS = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
keygen: true,
link: true,
menuitem: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true
};

function isVoidDOMElement(elementName) {
return has(VOID_DOM_ELEMENTS, elementName);
}

function errorMessage(elementName) {
return 'Void DOM element <' + elementName + ' /> cannot receive children.';
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'Prevent passing of children to void DOM elements (e.g. <br />).',
category: 'Best Practices',
recommended: false
},
schema: []
},

create: function(context) {
return {
JSXElement: function(node) {
var elementName = node.openingElement.name.name;

if (!isVoidDOMElement(elementName)) {
// e.g. <div />
return;
}

if (node.children.length > 0) {
// e.g. <br>Foo</br>
context.report({
node: node,
message: errorMessage(elementName)
});
}

var attributes = node.openingElement.attributes;

var hasChildrenAttributeOrDanger = !!find(attributes, function(attribute) {
if (!attribute.name) {
return false;
}

return attribute.name.name === 'children' || attribute.name.name === 'dangerouslySetInnerHTML';
});

if (hasChildrenAttributeOrDanger) {
// e.g. <br children="Foo" />
context.report({
node: node,
message: errorMessage(elementName)
});
}
},

CallExpression: function(node) {
if (node.callee.type !== 'MemberExpression') {
return;
}

if (node.callee.property.name !== 'createElement') {
return;
}

var args = node.arguments;
var elementName = args[0].value;

if (!isVoidDOMElement(elementName)) {
// e.g. React.createElement('div');
return;
}

var firstChild = args[2];
if (firstChild) {
// e.g. React.createElement('br', undefined, 'Foo')
context.report({
node: node,
message: errorMessage(elementName)
});
}

var props = args[1].properties;

var hasChildrenPropOrDanger = !!find(props, function(prop) {
if (!prop.key) {
return false;
}

return prop.key.name === 'children' || prop.key.name === 'dangerouslySetInnerHTML';
});

if (hasChildrenPropOrDanger) {
// e.g. React.createElement('br', { children: 'Foo' })
context.report({
node: node,
message: errorMessage(elementName)
});
}
}
};
}
};
96 changes: 96 additions & 0 deletions tests/lib/rules/void-dom-elements-no-children.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @fileoverview Tests for void-dom-elements-no-children
* @author Joe Lencioni
*/

'use strict';

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

var rule = require('../../../lib/rules/void-dom-elements-no-children');
var RuleTester = require('eslint').RuleTester;

var parserOptions = {
ecmaVersion: 6,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true
}
};

function errorMessage(elementName) {
return 'Void DOM element <' + elementName + ' /> cannot receive children.';
}

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

var ruleTester = new RuleTester();
ruleTester.run('void-dom-elements-no-children', rule, {
valid: [
{
code: '<div>Foo</div>;',
parserOptions: parserOptions
},
{
code: '<div children="Foo" />;',
parserOptions: parserOptions
},
{
code: '<div dangerouslySetInnerHTML={{ __html: "Foo" }} />;',
parserOptions: parserOptions
},
{
code: 'React.createElement("div", {}, "Foo");',
parserOptions: parserOptions
},
{
code: 'React.createElement("div", { children: "Foo" });',
parserOptions: parserOptions
},
{
code: 'React.createElement("div", { dangerouslySetInnerHTML: { __html: "Foo" } });',
parserOptions: parserOptions
}
],
invalid: [
{
code: '<br>Foo</br>;',
errors: [{message: errorMessage('br')}],
parserOptions: parserOptions
},
{
code: '<br children="Foo" />;',
errors: [{message: errorMessage('br')}],
parserOptions: parserOptions
},
{
code: '<img {...props} children="Foo" />;',
errors: [{message: errorMessage('img')}],
parserOptions: parserOptions
},
{
code: '<br dangerouslySetInnerHTML={{ __html: "Foo" }} />;',
errors: [{message: errorMessage('br')}],
parserOptions: parserOptions
},
{
code: 'React.createElement("br", {}, "Foo");',
errors: [{message: errorMessage('br')}],
parserOptions: parserOptions
},
{
code: 'React.createElement("br", { children: "Foo" });',
errors: [{message: errorMessage('br')}],
parserOptions: parserOptions
},
{
code: 'React.createElement("br", { dangerouslySetInnerHTML: { __html: "Foo" } });',
errors: [{message: errorMessage('br')}],
parserOptions: parserOptions
}
]
});