diff --git a/README.md b/README.md index 5137ca0fe3..01897602fa 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. ``, `
`) 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 0000000000..9bbdda7f90 --- /dev/null +++ b/docs/rules/void-dom-elements-no-children.md @@ -0,0 +1,30 @@ +# Prevent void DOM elements (e.g. ``, `
`) 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 +
Children
+
+
+React.createElement('br', undefined, 'Children') +React.createElement('br', { children: 'Children' }) +React.createElement('br', { dangerouslySetInnerHTML: { __html: 'HTML' } }) +``` + +The following patterns are not considered warnings: + +```jsx +
Children
+
+
+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 df651dfbd1..aac6284a25 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 0000000000..323dbed3b2 --- /dev/null +++ b/lib/rules/void-dom-elements-no-children.js @@ -0,0 +1,141 @@ +/** + * @fileoverview Prevent void elements (e.g. ,
) 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.
).', + category: 'Best Practices', + recommended: false + }, + schema: [] + }, + + create: function(context) { + return { + JSXElement: function(node) { + var elementName = node.openingElement.name.name; + + if (!isVoidDOMElement(elementName)) { + // e.g.
+ return; + } + + if (node.children.length > 0) { + // e.g.
Foo
+ 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.
+ 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) + }); + } + } + }; + } +}; 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 0000000000..022d8e877d --- /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: '
Foo
;', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + }, + { + code: '
;', + 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: '
Foo
;', + errors: [{message: errorMessage('br')}], + parserOptions: parserOptions + }, + { + code: '
;', + errors: [{message: errorMessage('br')}], + parserOptions: parserOptions + }, + { + code: ';', + errors: [{message: errorMessage('img')}], + parserOptions: parserOptions + }, + { + code: '
;', + 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 + } + ] +});