Skip to content

Commit

Permalink
Add void-dom-elements-no-children rule
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lencioni committed Jan 29, 2017
1 parent e6fdd02 commit 67d62bd
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 0 deletions.
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
}
]
});

0 comments on commit 67d62bd

Please sign in to comment.