diff --git a/README.md b/README.md index fb08b1cb6a..3bb37e6237 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](# * [react/display-name](docs/rules/display-name.md): Prevent missing `displayName` in a React component definition * [react/forbid-prop-types](docs/rules/forbid-prop-types.md): Forbid certain propTypes +* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing children as props * [react/no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX properties * [react/no-deprecated](docs/rules/no-deprecated.md): Prevent usage of deprecated methods * [react/no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of `setState` in `componentDidMount` diff --git a/docs/rules/no-children-prop.md b/docs/rules/no-children-prop.md new file mode 100644 index 0000000000..e49c7d8715 --- /dev/null +++ b/docs/rules/no-children-prop.md @@ -0,0 +1,36 @@ +# Prevent passing of children as props (no-children-prop) + +Children should always be actual children, not passed in as a prop. + +When using JSX, the children should be nested between the opening and closing +tags. When not using JSX, the children should be passed as additional +arguments to `React.createElement`. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +
+ +} /> + + +React.createElement("div", { children: 'Children' }) +``` + +The following patterns are not considered warnings: + +```jsx +
Children
+ +Children + + + Child 1 + Child 2 + + +React.createElement("div", {}, 'Children') +React.createElement("div", 'Child 1', 'Child 2') +``` diff --git a/index.js b/index.js index a28494f79e..effbfa0a21 100644 --- a/index.js +++ b/index.js @@ -50,7 +50,8 @@ var rules = { 'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'), 'jsx-filename-extension': require('./lib/rules/jsx-filename-extension'), 'require-optimization': require('./lib/rules/require-optimization'), - 'no-find-dom-node': require('./lib/rules/no-find-dom-node') + 'no-find-dom-node': require('./lib/rules/no-find-dom-node'), + 'no-children-prop': require('./lib/rules/no-children-prop') }; var ruleNames = Object.keys(rules); diff --git a/lib/rules/no-children-prop.js b/lib/rules/no-children-prop.js new file mode 100644 index 0000000000..efead3b2f7 --- /dev/null +++ b/lib/rules/no-children-prop.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Prevent passing of children as props + * @author Benjamin Stepp + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Checks if the node is a createElement call with a props literal. + * @param {ASTNode} node - The AST node being checked. + * @returns {Boolean} - True if node is a createElement call with a props + * object literal, False if not. +*/ +function isCreateElementWithProps(node) { + return node.callee + && node.callee.type === 'MemberExpression' + && node.callee.property.name === 'createElement' + && node.arguments.length > 1 + && node.arguments[1].type === 'ObjectExpression'; +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: {}, + schema: [] + }, + create: function(context) { + return { + JSXAttribute: function(node) { + if (node.name.name !== 'children') { + return; + } + + context.report({ + node: node, + message: 'Do not pass children as props. Instead, nest children between the opening and closing tags.' + }); + }, + CallExpression: function(node) { + if (!isCreateElementWithProps(node)) { + return; + } + + var props = node.arguments[1].properties; + var childrenProp = props.find(function(prop) { + return prop.key.name === 'children'; + }); + + if (childrenProp) { + context.report({ + node: node, + message: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.' + }); + } + } + }; + } +}; diff --git a/tests/lib/rules/no-children-prop.js b/tests/lib/rules/no-children-prop.js new file mode 100644 index 0000000000..d0073dc0f2 --- /dev/null +++ b/tests/lib/rules/no-children-prop.js @@ -0,0 +1,237 @@ +/** + * @fileoverview Tests for no-children-prop + * @author Benjamin Stepp + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +var rule = require('../../../lib/rules/no-children-prop'); +var RuleTester = require('eslint').RuleTester; + +var parserOptions = { + ecmaVersion: 6, + ecmaFeatures: { + jsx: true + } +}; + +var JSX_ERROR = 'Do not pass children as props. Instead, nest children between \ +the opening and closing tags.'; +var CREATE_ELEMENT_ERROR = 'Do not pass children as props. Instead, pass them \ +as additional arguments to React.createElement.'; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +var ruleTester = new RuleTester(); +ruleTester.run('no-children-prop', rule, { + valid: [ + { + code: '
;', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {});', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", undefined);', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {className: "class-name"});', + parserOptions: parserOptions + }, + { + code: '
Children
;', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", "Children");', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {}, "Children");', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", undefined, "Children");', + parserOptions: parserOptions + }, + { + code: '
Children
;', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {className: "class-name"}, "Children");', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", React.createElement("div"));', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {}, React.createElement("div"));', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", undefined, React.createElement("div"));', + parserOptions: parserOptions + }, + { + code: '
;', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", React.createElement("div"), React.createElement("div"));', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {}, React.createElement("div"), React.createElement("div"));', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", undefined, React.createElement("div"), React.createElement("div"));', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", [React.createElement("div"), React.createElement("div")]);', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {}, [React.createElement("div"), React.createElement("div")]);', + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", undefined, [React.createElement("div"), React.createElement("div")]);', + parserOptions: parserOptions + }, + { + code: '', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent);', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, {});', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, undefined);', + parserOptions: parserOptions + }, + { + code: 'Children;', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, "Children");', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, {}, "Children");', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, undefined, "Children");', + parserOptions: parserOptions + }, + { + code: ';', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, {className: "class-name"});', + parserOptions: parserOptions + }, + { + code: 'Children;', + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, {className: "class-name"}, "Children");', + parserOptions: parserOptions + } + ], + invalid: [ + { + code: '
;', + errors: [{message: JSX_ERROR}], + parserOptions: parserOptions + }, + { + code: '
} />;', + errors: [{message: JSX_ERROR}], + parserOptions: parserOptions + }, + { + code: '
,
]} />;', + errors: [{message: JSX_ERROR}], + parserOptions: parserOptions + }, + { + code: '
Children
;', + errors: [{message: JSX_ERROR}], + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {children: "Children"});', + errors: [{message: CREATE_ELEMENT_ERROR}], + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {children: "Children"}, "Children");', + errors: [{message: CREATE_ELEMENT_ERROR}], + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {children: React.createElement("div")});', + errors: [{message: CREATE_ELEMENT_ERROR}], + parserOptions: parserOptions + }, + { + code: 'React.createElement("div", {children: [React.createElement("div"), React.createElement("div")]});', + errors: [{message: CREATE_ELEMENT_ERROR}], + parserOptions: parserOptions + }, + { + code: '', + errors: [{message: JSX_ERROR}], + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, {children: "Children"});', + errors: [{message: CREATE_ELEMENT_ERROR}], + parserOptions: parserOptions + }, + { + code: ';', + errors: [{message: JSX_ERROR}], + parserOptions: parserOptions + }, + { + code: 'React.createElement(MyComponent, {children: "Children", className: "class-name"});', + errors: [{message: CREATE_ELEMENT_ERROR}], + parserOptions: parserOptions + } + ] +});