Repository: yannickcr/eslint-plugin-react (js)

Issue:
Rule proposal: prevent some DOM elements from having children or dangerouslySetInnerHTML
There are some HTML elements that are normally self-closing (e.g. `img`, `br`, `hr`). 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`.

It would be nice to have a rule enforcing this.

Bad:

``` jsx
<img src="path/to/img.jpg">
  Children
</img>
```

``` jsx
<img src="path/to/img.jpg" dangerouslySetInnerHTML={{ __html: 'HTML' }} />
```

``` jsx
<img src="path/to/img.jpg" children="Children" />
```

Good:

``` jsx
<img src="path/to/img.jpg" />
```

The full list that React uses can be found:
- https://github.com/facebook/react/blob/85dcbf83/src/renderers/dom/shared/ReactDOMComponent.js#L421
- https://github.com/facebook/react/blob/85dcbf83/src/renderers/dom/shared/ReactDOMComponent.js#L449

Which comes out as:
- `area`
- `base`
- `br`
- `col`
- `embed`
- `hr`
- `img`
- `input`
- `keygen`
- `link`
- `menuitem`
- `meta`
- `param`
- `source`
- `track`
- `wbr`


Pull request description:
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

Patch:
diff --git a/README.md b/README.md
index 5137ca0f..01897602 100644
--- a/README.md
+++ b/README.md
@@ -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
 
diff --git a/docs/rules/void-dom-elements-no-children.md b/docs/rules/void-dom-elements-no-children.md
new file mode 100644
index 00000000..9bbdda7f
--- /dev/null
+++ b/docs/rules/void-dom-elements-no-children.md
@@ -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' } })
+```
diff --git a/index.js b/index.js
index df651dfb..aac6284a 100644
--- a/index.js
+++ b/index.js
@@ -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'),
diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js
new file mode 100644
index 00000000..323dbed3
--- /dev/null
+++ b/lib/rules/void-dom-elements-no-children.js
@@ -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)
+          });
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/void-dom-elements-no-children.js b/tests/lib/rules/void-dom-elements-no-children.js
new file mode 100644
index 00000000..022d8e87
--- /dev/null
+++ b/tests/lib/rules/void-dom-elements-no-children.js
@@ -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
+    }
+  ]
+});